V8垃圾回收原理
背景介绍
V8是Google的Chrome浏览器中的JS引擎,实现了ECMAScript规范,寄生于浏览器环境中,负责解释执行JS以及执行垃圾回收。
JS中垃圾回收是自动执行的,不需要开发人员手动去执行。
JS中什么时候触发垃圾回收:
- 块级作用域(如函数)中定义的数据,在块级作用域失效后就会被回收
- 失活的对象会被回收
NOTE
具体的回收时间是不固定的 不是说失活的对象会被立即回收。毕竟垃圾回收也是在渲染进程的主线程上运行的,和JS代码是互斥的。
function test() {
const a = 123
return a
}
test()
// test执行完成之后,a会被回收
let obj = new Array()
obj = {}
// new Array()会被回收常见回收算法
ECMAScript规范并没有明确说明JS引擎应该用什么算法来回收垃圾,不过一般有几种常见的算法。
引用计数
堆中的对象有一个引用计数器,被初始化赋值后计数器就置为1,有其他地方引用后,计数器加1;引用失效后,计数器减1。
var obj = new Object() // 计数器1
var otherObj = obj // 计数器加1 为2
otherObj = null // 引用失效 计数器减1,为1
obj = null // 引用失效 计数器减1,为0当计数器为0时,就会被当做垃圾进行回收。
优点
计算器实现简单,判断效率高,性能也比较好。
缺点
每次更新计数器,增加了时间开销。最大的缺点是无法处理循环引用的问题。
循环引用时,互相引用的变量会导致计数器无法清空,即使对象已经不可访问,会导致内存泄露。解决方法就是使用弱引用,这样不会计数。
根搜索Tracing Collector(可访问性)
GC Roots Set根集
可访问的引用集合。该集合中的引用变量可以访问到对象属性以及调用对象方法。
根搜索算法是以一系列的GC Roots对象为起点,递归遍历找到所有的可访问节点。
如果一个对象从GC Roots找不到时,说明该对象不可访问,是一个垃圾数据。
循环引用时,Roots Set是一个图,其余情况是一个树形结构。
以下对象可以当做根节点:
- 栈中所有正在运行的引用变量
- 所有全局对象和全局变量
- 所有内置对象
大致流程是这样的:内存中对整个堆进行遍历,从GC Roots根对象开始,然后递归遍历所有可到达的对象,可访问的对象标记为存活状态。
NOTE
GC判断对象是否可访问,判断的是强应用,弱引用不会判断。
V8的垃圾回收算法思路
V8采用的是可访问性(reachability)算法,具体如下:
- 通过
GC Roots标记内存空间中的活动对象和非活动对象简单而言就是从一个根对象出发,遍历所有的对象,将可以遍历到的对象标记为活动对象,对于不可遍历到的对象称为非活动对象。垃圾回收的就是非活动对象
根对象可能是(不仅是):
- 全局window对象
- 文档DOM树
- 存放栈上变量
- 回收
非活动对象的空间:标记完成之后,统一回收非活动对象的空间。 - 内存整理 频繁的垃圾回收之后,可能会造成很多不连续的内存空间,这些不连续的内存空间称为
内存碎片。但是有的垃圾回收可能并不会导致内存碎片,所以这一步可选
V8的垃圾回收具体实现
代际假说(The Generational Hypothesis)
代际假说是垃圾回收领域的一个术语,主要有两个特点:
- 大部分对象都是"朝生夕死"的。大部分对象在内存中存在的时间都是很短的,随着函数或代码的执行完毕,会很快被销毁。
- 不死的对象,会存活的更久。
简单总结一下就是:大部分的对象是会很快被回收的,如果没有被回收大概率会存活很久。
V8的垃圾回收架构
V8根据代际假说,将堆分为两个区域,一个新生代young space,一个老生代old space。新生代中容量较小,一般只有1-8M,存放一些生存时间较短的对象;老生代中容量比较大,存放生存时间较长或者占用空间很大的对象
根据堆的不同,会有两个垃圾回收器,主垃圾回收器以及副垃圾回收器
- 主垃圾回收器-Major GC,负责回收老生代中的数据
- 副垃圾回收器-Minor GC,负责回收新生代中的数据
副垃圾回收器
新生代中存放的一些生存时间较短的对象,所以会有频繁的垃圾回收
采用Scavenge算法来处理,将新生代对半划分为两个区域,一个区域叫做对象区from-space,一个区域叫做空闲区to-space。等到对象区快要被填满时,就执行一次垃圾回收。 在垃圾回收时,首先对对象区域中的垃圾数据进行标记;标记完成之后,进入垃圾清理阶段,将非垃圾数据复制一份按一定顺序存入空闲区域,这样既完成了清理垃圾也完成了内存整理的工作;然后将对象区域清空掉,再将对象区域和空闲区域调换位置。这种角色互换可以让这两块区域重复使用
新生代区域中执行垃圾回收,会有一次复制操作,会很耗时,这也是为什么新生代空间不能设置太大的原因之一,还有一个原因是,内存小就很容易被占满,垃圾回收的频率会比较高
此外,还有一个对象晋升策略,对于经过了两次垃圾回收的对象,会转入老生代中存储
主垃圾回收器
老生代中存放一些大对象和生存时间较长的对象
主垃圾回收器采用的是标记-清除/标记-整理算法来进行垃圾回收
标记-清除:
- 标记阶段 从根元素(GC Roots对象)开始,遍历元素,能到达的元素标记为活动对象,不能到达的元素称为非活动对象
- 清理阶段 主垃圾回收器会直接清除掉垃圾数据
不过这样会产生大量的内存碎片,所以又引入了一个标记-整理算法来清除内存碎片:标记阶段和标记-清除算法一致,但是清理阶段不会直接清除数据,而是将活动对象全部移向同一端,然后直接清空掉另一端的空间

总结
V8的垃圾回收是基于代际假说的,分为主垃圾回收器和副垃圾回收器两种
副垃圾回收器采用Scavenge算法,分为对象区域和空闲区域,等待对象区域满了之后执行垃圾回收,标记垃圾对象,将非垃圾对象复制到空闲区域,然后将两个区域调换达到复用的目的;
主垃圾回收器采用标记-清除/标记-整理算法,标记阶段从根元素开始对元素遍历,将元素分为活动对象和非活动对象,清理阶段直接将非活动对象清除,但是这样会导致大量的内存碎片。所以有了标记-整理算法,不同之处在于清理阶段不会直接清除非活动对象,而是将活动对象全都移到同一侧,将另一侧的空间清除掉,达到了清理和整理两个目的