V8 之分代回收策略
V8 引擎(以及其他现代垃圾回收器)采用分代垃圾回收策略,其核心思想基于一个观察:在大多数程序中,绝大多数对象都是“朝生夕死”的(生命周期很短),只有少数对象会存活较长时间。
这个观察被称为弱分代假说。分代回收策略就是利用这一特点,将堆内存划分为不同的“代”,并对不同代采用不同的、更高效的垃圾回收算法,从而显著减少垃圾回收的总体开销和停顿时间。
V8 分代回收的核心机制
代际划分:
- 新生代: 新创建的对象首先被分配在这里。新生代区域相对较小(通常在 1MB 到 8MB 之间)。这个区域的对象生命周期极短,大部分很快变得不可达。
- 老生代: 在新生代中经历过一定次数垃圾回收后仍然存活下来的对象,会被晋升到老生代。老生代区域要大得多(可达几百 MB 甚至 GB),存放生命周期较长的对象。老生代中也包含一些直接分配的大对象(避免在新生代中复制)。
针对不同代的回收策略:
新生代回收 (Minor GC / Scavenge):
- 算法: 主要采用 Scavenge 算法(一种Cheney算法的实现),这是一种复制算法。
- 过程:
- 将新生代空间等分为两块:
From-Space
(活动区/对象区) 和To-Space
(空闲区/空闲区)。 - 新对象只分配在
From-Space
。 - 当
From-Space
快满时,触发一次 Minor GC。 - 垃圾回收器从根对象(全局变量、活动函数栈上的变量等)开始扫描,标记所有在
From-Space
中可达的对象。 - 将标记为可达的对象复制到
To-Space
中,并紧密排列(消除内存碎片)。 - 复制完成后,清除整个
From-Space
(因为剩下的都是不可达的垃圾)。 - 交换
From-Space
和To-Space
的角色。原来的To-Space
变为新的From-Space
(用于新对象分配),原来的From-Space
变为新的空闲To-Space
。
- 将新生代空间等分为两块:
- 特点: 速度非常快(只处理活动对象,不处理死对象;操作在连续空间内进行,效率高),停顿时间短。但空间利用率只有 50%(因为需要一半空间作为空闲区)。适用于频繁回收大量短命对象的场景。
- 对象晋升: 如果一个对象在新生代中经历了一次 Minor GC 后仍然存活,或者
To-Space
空间不足以容纳所有要复制的存活对象,那么这个对象会被晋升到老生代。
老生代回收 (Major GC / Full GC):
- 算法: 主要采用 标记-清除 (Mark-Sweep) 和 标记-整理 (Mark-Compact) 的组合。通常增量标记、并发标记/清除等优化技术会应用在这里以减少停顿。
- 过程:
- 标记阶段: 从根对象开始,递归遍历对象图,标记所有可达的老生代对象。
- 清除阶段 (Mark-Sweep): 遍历整个老生代堆,回收所有未被标记的对象所占用的内存。回收后内存空间是不连续的(有碎片)。
- 整理阶段 (Mark-Compact - 可选): 为了解决 Mark-Sweep 产生的碎片问题,V8 有时(当碎片达到一定程度时)会在 Mark-Sweep 之后或代替 Mark-Sweep 进行 Mark-Compact。它会将所有存活的对象移动到堆的一端,紧密排列,然后清理掉边界外的所有空间。这样回收的空间是连续的。
- 特点: 回收整个老生代,速度相对较慢(对象多、存活率高、对象图复杂),停顿时间长(但通过增量标记、并发回收等优化可以显著减少感知停顿)。空间利用率高(Mark-Compact 后无碎片)。适用于回收生命周期长的对象和低频回收。
代际引用与写屏障:
- 问题: 分代策略的一个关键挑战是代际引用。老生代中的对象可能持有指向新生代对象的引用。如果只扫描新生代本身,那么一个被老生代对象引用的新生代对象会被认为是可达的(即使它本应在新生代回收中被回收),反之,如果只回收老生代,可能漏掉仅被新生代引用的老生代对象(但这种情况较少)。
- 解决方案:写屏障:
- V8 引入了一种称为写屏障的机制。当 JavaScript 代码写入一个对象的属性(即修改引用关系)时,V8 会检查:
- 被写入属性的对象是否位于老生代。
- 被赋值的新值是否是一个新生代中的对象。
- 如果以上两个条件都满足,V8 会将这个老生代对象记录下来(例如,将其加入一个特殊的“
store buffer
”或“card table
”)。
- V8 引入了一种称为写屏障的机制。当 JavaScript 代码写入一个对象的属性(即修改引用关系)时,V8 会检查:
- 作用: 在 Minor GC 进行可达性扫描时,除了扫描新生代内部的引用和根对象,还需要将记录了的老生代对象(通过写屏障记录的)作为额外的根来扫描。这确保了即使新生代对象只被老生代引用,也不会在 Minor GC 中被错误回收。这解决了“老生代引用新生代”的问题,保证了新生代回收的正确性。老生代回收通常需要扫描整个堆(包括新生代),或者利用类似机制处理反向引用。
分代回收的优势
- 提高吞吐量: 大部分短命对象在快速高效的新生代回收中被回收,避免了在老生代上执行昂贵的回收操作。
- 减少停顿时间: Minor GC 速度非常快,停顿时间很短。虽然 Major GC 停顿较长,但发生的频率远低于 Minor GC(因为老生代对象存活率高,变化慢)。优化技术(增量标记、并发回收)主要应用在 Major GC 上进一步降低停顿。
- 空间局部性: Scavenge 算法通过复制整理存活对象,使它们紧密排列在连续的内存中,提高了 CPU 缓存命中率。标记-整理也有类似效果。
总结
V8 的分代垃圾回收策略是其高性能的关键之一。它通过将堆划分为新生代和老生代,并针对不同代对象的生存特点采用不同的回收算法(Scavenge 用于新生代,Mark-Sweep/Mark-Compact 用于老生代),显著提高了垃圾回收的效率,减少了应用停顿时间。写屏障机制的引入巧妙地解决了代际引用带来的正确性问题,使得分代策略能够安全高效地运行。