V8 垃圾回收机制
1. 什么是垃圾
垃圾通常是指非活动对象:即不再使用的对象
let obj = { name: 'obj1' }
obj = { name: 'obj2' } // 当该行代码执行完毕后,由于 { name: 'obj1' } 并不会被使用,故而认定其为非活动对象,会被垃圾回收器回收
2. 垃圾回收算法
目前 V8 采用可访问性(reachability)算法来判断是否为活动对象
- 标记非活动对象
- 从 GC Root 出发,遍历 GC Root 中所有的对象
- 能被遍历到的对象被认定为活动对象
- 否则被认定为非活动对象
- 清理所有的非活动对象
- 内存整理:一般情况下,经过频繁的垃圾回收后,会产生大量的内存碎片。此时如果想要分配一块连续较大的空间,就很有可能会面临内存空间不足的情况:解决方法就是进行内存整理,腾出足够的连续内存空间
- 内存整理之前/后
- 内存整理之后
- 内存整理之前/后
2.1 代际假说
代际假说有两个特点:
- 大部分的对象在内存中的存活时间很短, 如自己创建的一些对象, 变量
- 存活超过一定时间的对象很可能会活得更久, 如:
window
,document
介于代际假说,V8 采用了两种垃圾回收器: 副垃圾回收器负责对新生区进行垃圾回收, 而主垃圾回收器负责对老生区进行垃圾回收
2.2 副垃圾回收器
副垃圾回收器是专门用来处理存活时间较短的对象的, 其支持1~8M的容量(该空间被称为新生区), 并将新生区分为对象区和空闲区两个区域, 且新创建的对象都会被分配到对象区中
2.2.1 Scanvenge 算法的执行
副垃圾回收器使用的是 Scanvenge 算法. 在执行 Scanvenge 算法时, 其有如下的步骤
- 标记对象区中的活跃对象
- 将对象区中的活跃对象有序地复制排列到空闲区中
- 令空闲区和对象区角色互换
2.2.2 对象晋升
当新生区中的对象经过两次垃圾回收后仍没有被清除, 介于代际假说, 判定其会生存更长的时间, 故而将其移动至老生区中
2.3 主垃圾回收器
2.3.1 垃圾回收算法
主垃圾回收器组合采用的垃圾回收算法为:
- 标记-清除算法
- 主垃圾回收器会通过对 GC Root 的遍历, 对活动对象和非活动对象进行标记
- 直接对非活动对象进行清除
- 标记-整理算法: 在进行多次标记-清除算法后, 难以避免会产生一定数量的内存碎片. 通过标记-整理算法可以对内存进行整理, 从而清除内存碎片
- 通过对 GC Root 的遍历, 对活动对象和非活动对象进行标记
- 让活动对象向一端移动, 形成一块连续的, 由活动对象占据的内存空间
- 直接清理掉活动对象一端之外的其他内存空间
缺点: 由于 JS 代码是执行在主线程之上的, 当需要执行垃圾回收算法时, JS 脚本会停止执行, 待垃圾回收算法执行完毕之后, 才会重新继续运行. 这种行为被称作全停顿(Stop-The-World)
如下图所示, 主线程会有 200ms 的时间用来进行垃圾回收, 在这个过程中, 任何 JS 代码都无法执行, 页面陷入卡顿状态, 会造成不良的用户体验
2.3.2 全停顿的优化
2.3.2.1 并行回收
在进行垃圾回收时, 不仅仅只有主线程在运作, 还有多个辅助线程帮助进行标记和清理, 从而使得执行垃圾回收算法所需要的时间大大减少(V8 副垃圾回收器采用的就是这样的策略)
2.3.2.2 增量回收
但是并行回收仍然会造成全停顿, 且老生区的大对象很多, 即使有辅助线程仍然也需要很久的时间.
对此, V8 引入了增量回收策略: 将垃圾回收算法切割为很多小段进行执行, 每次只执行一小段, 这样就不会发生长时间的停顿从而影响用户体验了
要实现增量回收, 需要满足以下两点要求:
- 垃圾回收可以被随时暂停和启动, 暂停时需要保存当时的状态, 等待下一次垃圾回收时启用
- 三色标记: 对于增量回收, 其在遍历 GC Root 时
- 如果一个节点和它的子节点都能被 GC Root 引用到, 则其被标记为黑色
- 如果一个节点能被 GC Root 引用到, 而其子节点还没有被标记, 则将该节点被标记为灰色, 用来表示该节点是"正在处理阶段"
- 如果一个节点无法被 GC Root 引用到, 则默认其为白色
- 引入三色标记后, 通过查看灰色节点的存在与否就能够知道标记阶段是否完成
- 如果没有灰色节点, 则标记完成. 可以开始进行清理工作: 将白色节点全部回收
- 如果存在灰色节点, 则从灰色节点开始, 继续进行标记工作
- 三色标记: 对于增量回收, 其在遍历 GC Root 时
- 在暂停期间, 如果被标记的垃圾数据被 JS 代码修改了, 那么垃圾回收器需要能够正确地处理
- 写屏障(Write-barrier): 如果一个黑色节点的子节点为白色, 则自动将白色转换为灰色
- 标记结束:
- JS 代码的修改:
- 写屏障:
- 写屏障(Write-barrier): 如果一个黑色节点的子节点为白色, 则自动将白色转换为灰色
但是对内存压力较大的堆, 垃圾回收器仍然可能出现长时间的暂停来维持分配
2.3.2.3 并发标记
并发标记主要发生在辅助线程上. 当并发标记正在进行时, 主线程可以继续运行