17 垃圾回收:V8的垃圾回收器
V8 为了更高效地回收垃圾,引入了两个垃圾回收器,分别针对不同的场景。
垃圾数据是怎么产生的
数据会被存放到栈和堆中,通常的方式是在内存中创建一块空间,使用这块空间,在不需要的时候回收这块空间。
window.test = new Object();
window.test.a = new Uint16Array(100);
为 window 对象添加一个 test 属性,并在堆中创建了一个空对象,将该对象的地址指向 window.test 属性。随后又创建一个大小为 100 的数组,并将属性地址指向了 test.a 的属性值。
window.test.a = new Object();
a 属性之前指向堆中数组对象,现在已经指向了另外一个空对象,此时堆中的数组对象就成为了垃圾数据,因为无法从一个根对象遍历到这个 Array 对象。V8 虚拟机中的垃圾回收器就会帮忙自动清理该数据。
垃圾回收算法
通过 GC Root 标记空间中活动对象和非活动对象
V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:
- 通过 GC Root 遍历到的对象,就认为该对象是可访问的(reachable),这些对象应该在内存中保留,称可访问的对象为活动对象;
- 通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),这些不可访问的对象就可能被回收,称不可访问的对象为非活动对象。
在浏览器环境中的 GC Root:
- 全局的 window 对象(位于每个 iframe 中);
- 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
- 存放栈上变量。
回收非活动对象所占据的内存
在 所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
做内存整理
频繁回收对象后,内存中会存在大量不连续空间,称为内存碎片。如果需要分配较大的连续内存时,有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。有些垃圾回收器不会产生内存碎片。
受到代际假说(The Generational Hypothesis)的影响,V8 采用了两个垃圾回收器,主垃圾回收器 -Major GC 和副垃圾回收器 -Minor GC (Scavenger)。
代际假说的两个特点:
- 大部分对象在内存中存活的时间很短(比如函数内部声明的变量,块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁)。这一类对象一经分配内存,很快就变得不可访问;
- 小部分不死的对象,会活得很久(比如全局的 window、DOM、Web API 等对象)。
只使用一个垃圾回收器,优化大多数新对象时,很难优化到老对象,因此需要权衡各种场景,根据对象生存周期的不同,而使用不同的算法。因此 V8 把堆分为新生代和老生代两个区域,新生代中存放生存时间短的对象,老生代中存放生存时间久的对象。
新生代通常只支持 1 ~ 8M 的容量,老生代支持的容量大很多。V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
- 副垃圾回收器 -Minor GC (Scavenger),主要负责新生代的垃圾回收。
- 主垃圾回收器 -Major GC,主要负责老生代的垃圾回收。
副垃圾回收器
副垃圾回收器主要负责新生代的垃圾回收。大多数小的对象都会被分配到新生代,这个区域虽然不大,但是垃圾回收比较频繁。
新生代中的垃圾数据用 Scavenge 算法来处理,把新生代空间对半划分为两个区域,一半是对象区域 (from-space),一半是空闲区域 (to-space):
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
垃圾回收时,先对对象区域中的垃圾做标记,然后把这些存活的对象复制到空闲区域中,同时把这些对象有序地排列起来,复制过程相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
完成复制后,原来的对象区域变成空闲区域,原来的空闲区域变成对象区域。就完成了垃圾对象的回收操作,同时让新生代中的这两块区域无限重复使用下去。
副垃圾回收器会采用对象晋升策略,移动那些经过两次垃圾回收依然还存活的对象到老生代中。