谈谈垃圾回收机制

470 阅读11分钟

gc.jpeg

前言

作为一名程序猿(媛)肯定听过垃圾回收机制(如果没有听过,那就当我没说吧~ but,你就好好看看这篇文章吧~),但是你有好好了解过什么是垃圾回收机制么?知道垃圾是怎么产生的么?知道为什么要进行垃圾回收以及怎么样进行垃圾回收的么?如果这些问题你都能非常清楚的回答出来的话,那可以省下看这篇文章的时间去学习其他的啦~。如果还不是很清楚以上的问题,就需要认真的以下内容啦

什么是垃圾&垃圾产生

在智能手机刚刚流行起来的时候,我们肯定都遇又到过这样的经历:那就是需要经常清理手机,否则就会提示我们内存不足。就拿存储照片来说,可以把相薄想象成一个纸箱,我们会不断的拍照片存放到这个纸箱中,最终会有遇到纸箱装满再也存不下新照片的时候,那我们又想存储照片怎么办呢?这个时候我们就会去翻相薄把之前不用的照片给删除掉,然后纸箱又有了多余的空间来存放新照片了。
之前不用的照片我们就可以认为是产生的垃圾。
同样的,程序中那些不再被引用和使用的变量、函数等就被认为是产生的垃圾
那为什么会产生这些垃圾呢?
我们知道 JavaScript 的引用数据类型是保存在堆内存中的,然后在栈内存中保存一个对堆内存中实际对象的引用,所以,JavaScript 中对引用数据类型的操作都是操作对象的引用而不是实际的对象。可以简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的。
举个简单的栗子

let test = ['哈哈哈']
test = {
    title:'谈谈垃圾回收机制'
}

首先我们声明了一个变量 test,它引用了数组 [哈哈哈],接着我们把这个变量重新赋值了一个对象,也就变成了该变量引用了一个对象,那么之前的数组引用关系就没有了,如下图

image.png

没有了对数组的引用关系,这个时候数组就是无用的了,但是虽然无用也还是占用了空间,不会因为没有地方用它,它所占的空间就消失。如果说这样的对象只有一两个就还好,假如说太多的话就肯定不行,内存受不了,会影响正常的程序运行。那么这些就需要被回收,我们也就称之为这些是垃圾。

程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃

垃圾回收策略

通过上面的介绍呢,我们知道在内存中产生了垃圾就要进行回收,否则就会影响程序的正常性能。那么如何进行垃圾回收呢?在了解如何进行回收之前先来了解一个概念,就是JavaScript内存管理中的可达性

可达性

所谓的可达性就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收。
什么算可达的值呢?
例如,我们上面举得一个栗子test的引用,最初test被赋值成数组的时候,这个数组是有引用的,那么这个数组就是可达的,但是当test又被指向了一个新的对象的时候,此时原来的数组就没有地方在引用了,那么这个就是不可达的。
再比如一个更复杂的栗子:

function marry (man, woman) { 
   woman.husban = man; 
   man.wife = woman; 
   return { 
       father: man, 
       mother: woman 
   } 
} 
let family = marry({ name: "John" }, { name: "Ann" })

函数 marry 通过给两个对象彼此提供引用来连接它们,并返回一个包含两个对象的新对象。在内存中呢,会有一个如下的引用关系:

image.png 假如此时:delete father.wife,但是我们依然还是可以通过family.mother访问到在内存中创建的mother对象,此时mother依然是可达的;
但是假如此时再delete family.mother,那么内存中的mother对象就再也没有引用,此时就变成不可达的了。

1. 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:

  • 本地函数的局部变量和参数
  • 当前嵌套调用链上的其他函数的变量和参数
  • 全局变量
  • 还有一些其他的,内部的

这些值称为根。

2. 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。

垃圾回收策略

怎样发现这些不可达的对象(垃圾)并给予清理呢?这个流程就涉及到了一些算法策略,有很多种方式,最常见的两种是:

  • 标记清除算法
  • 引用计数算法

标记清除算法

标记清除(Mark-Sweep),目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。
此算法分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
引擎在执行 垃圾回收(使用标记清除算法)时,需要从出发点去遍历内存中所有的对象去打标记,而这个出发点有很多,我们称之为一组  对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象文档DOM树 等。
标记清除算法大致过程:

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

为什么是等待下一轮垃圾回收而不是实时进行呢?其实很简单,实时开销太大了

【优点】

实现比较简单,只需要判断是否打标记即可

【缺点】

标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题。

image.png 假设我们新建对象分配内存时需要大小为 size,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size 的块才能为其分配。 如何找到合适的内存块呢?可以使用

  • First-fit,找到大于等于 size 的块立即返回
  • Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块
  • Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回 由于内存分配的问题这也导致了标记清除算法还有另外一个缺点,那就是分配速度慢,如果遇到大对象的时候可能会更慢。

由于标记清除算法有以上的两个缺点:内存碎片化分配速度慢的问题,那么就出现了另一种方法,就是 标记清除整理算法(Mark-Sweep-Compact),也就是在标记清除的基础上增加了一个步骤:将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存

  • 标记阶段:收集器从GC Roots开始遍历所有可达对象,并且对这些存活的对象进行标记。
  • 清理阶段:收集器把所有未标记的对象进行清理和回收。
  • 压缩阶段:收集器把所有存活的对象移动到堆内存的起始端,然后清理掉端边界之外的内存空间。 image.png

引用计数算法

引用计数(Reference Counting),这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前很少使用这种算法了,因为它的问题很多,不过我们还是需要了解一下。
它的策略是跟踪记录每个变量值被使用的次数:

  • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
  • 如果同一个值又被赋给另一个变量,那么引用数加 1
  • 如果该变量的值被其他的值覆盖了,则引用次数减 1
  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存 我们会发现引用计数算法有一个很大的缺点,就是如果对象出现循环引用的时候,是没有办法被清除掉的。例如:
function test(){
    let A = new Object();
    let B = new Object();
    A.b = B;
    B.a = A;
}
test();

上面test执行完之后,按正常来说AB是应该会被删除掉,但是如果使用引用计数算法,会发现,它们的引用数是1而不是0,是没有办法被清理掉的。那么使用引用计数的话还是会有很多垃圾存在,这也是被废弃的原因之一吧~

【优点】

  • 首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾,而标记清除算法需要每隔一段时间进行一次
  • 另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了

【缺点】

  • 上面提到的无法解决循环引用产生的垃圾
  • 引用计数的缺点首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限

垃圾回收机制

讲了什么是垃圾,垃圾是如何产生以及为何和如何进行垃圾回收,那么垃圾回收机制这个应该也知道了吧。
GC 即 Garbage Collection ,程序工作过程中会产生很多 垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而 GC 就是负责回收垃圾的,因为他工作在引擎内部,所以对于我们前端来说,GC 过程是相对比较无感的,这一套引擎执行而对我们又相对无感的操作也就是常说的 垃圾回收机制 了

JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象
在上面提到的标记清除算法中,在执行GC的时候,JavaScript运行过程中就要暂停去执行GC。

结束语

通过以上的问题应该清楚开头提到的问题了吧:
什么是垃圾回收机制?

垃圾是怎样产生的?

为什么要进行垃圾回收?

垃圾回收是怎样进行的?

还有最后一个问题就是V8引擎对垃圾回收的优化?由于文章篇幅已经很长了,这个问题还是留在下篇吧~

参考文章
前端面试:谈谈 JS 垃圾回收机制
「硬核」你真的了解垃圾回收机制吗
深入理解Java中的Garbage Collection