JavaScript 中的垃圾回收:V8 引擎的 Minor GC(Scavenger) 与 Major GC(Mark-Compact)协同工作机制
在 V8 引擎的垃圾回收中,Minor GC(副垃圾回收) 和 Major GC(主垃圾回收) 是基于“代际假说”的两个核心回收过程,它们协同工作以高效管理内存。我会从基础概念开始,逐步深入它们的工作细节、协同机制和触发时机。
1. 核心背景:代际假说与分代内存布局
代际假说指出:
- 大多数对象生命周期很短(很快变得不可访问)。
- 少数对象会存活较长时间。
基于此,V8 将堆内存分为两代:
- 新生代:存放新创建的对象。特点是对象存活时间短、区域小、垃圾回收频繁。
- 老生代:存放存活时间较长的对象(从新生代晋升而来)。特点是对象存活时间长、区域大、垃圾回收不频繁。
2. 新生代回收:Minor GC(Scavenger)
目标:快速回收新生代中的短暂存活对象。
算法:Cheney 算法(一种复制算法,采用“半空间”设计)。
步骤详解:
步骤 1:内存划分
新生代被划分为两个相等大小的半空间:From-Space(使用中)和 To-Space(闲置)。对象始终只在 From-Space 中分配。
步骤 2:对象分配
新对象在 From-Space 中按顺序分配。当 From-Space 被填满时,触发 Minor GC。
步骤 3:标记活跃对象
从根对象(全局对象、当前函数作用域链等)出发,遍历引用链,标记 From-Space 中所有可访问的对象为“活跃对象”。
步骤 4:复制活跃对象
- 将
From-Space中的活跃对象复制到To-Space中,并紧密排列(消除内存碎片)。 - 复制过程中,会更新对象的指针,使其指向
To-Space中的新地址。
步骤 5:更新引用指针
- 由于对象被移动,所有指向这些对象的指针(包括老生代中的指针)都必须更新。V8 通过写屏障机制来记录跨代引用,确保引用正确更新。
步骤 6:角色交换
复制完成后,清空 From-Space 并交换 From-Space 和 To-Space 的角色。现在,To-Space 变为新的使用空间,From-Space 变为闲置空间。
步骤 7:对象晋升
在复制过程中,如果一个对象满足以下任一条件,会被晋升到老生代:
- 对象已经经历过一次 Minor GC 且仍然存活。
- 复制时,
To-Space空间不足(避免无限制复制)。
3. 老生代回收:Major GC(Mark-Compact)
目标:回收老生代中长期存活但已死亡的对象。
算法:结合标记-清除和标记-压缩,通常使用“增量标记”和“惰性清理”优化。
步骤详解:
步骤 1:标记阶段
- 从根对象出发,遍历所有可达对象,将它们标记为“活跃”。
- 由于老生代对象多,V8 使用增量标记:将标记过程分解为多个小步骤,穿插在 JavaScript 执行中,避免长时间停顿。
步骤 2:清除/压缩阶段
- 清除未被标记的对象(死亡对象),释放内存。
- 但频繁清除会产生内存碎片,因此 V8 会周期性执行标记-压缩:
- 将所有活跃对象向内存一端移动。
- 更新所有引用指针。
- 释放另一端连续内存,避免碎片。
步骤 3:写屏障的作用
- 在 Minor GC 和 Major GC 之间,写屏障 记录从老生代到新生代的引用,确保跨代引用的正确性。
- 在 Major GC 标记时,写屏障记录的引用会被作为“额外根”来扫描。
4. Minor GC 与 Major GC 的协同工作机制
触发时机:
- Minor GC:当新生代
From-Space满时触发,频繁且快速。 - Major GC:当老生代内存达到阈值时触发(或由调度算法决定),较慢但全面。
协同流程:
- 新对象在新生代分配 → 反复经历 Minor GC。
- 存活对象晋升到老生代。
- 老生代逐渐积累,触发 Major GC。
- Major GC 会同时处理新生代和老生代,但以老生代为主:
- 它从根对象开始标记,包括新生代对象。
- 由于跨代引用的存在,写屏障确保不会遗漏。
避免全堆回收:
- 通常 Major GC 只标记和清理老生代,但如果内存压力大,V8 可能执行全堆垃圾回收(包含两代)。
- 优化:V8 的 Orinoco 项目 实现了并发标记和并行清理,减少主线程停顿。
5. 举例说明协同过程
假设我们有以下代码:
function createObjects() {
let shortLived = { name: "temp" }; // 新生代分配
let longLived = { data: new Array(1000) }; // 新生代分配,但较大
return longLived; // longLived 被返回,可能被外部引用
}
let globalObj = createObjects();
- 初始:
shortLived和longLived在新生代。 - Minor GC 触发:
shortLived在函数结束后不可达 → 被回收。longLived被globalObj引用 → 活跃,晋升到老生代。
- 后续:
longLived在老生代长期存在,直到不可达时由 Major GC 回收。
6. 总结与关键点
- Minor GC:快速、频繁,使用复制算法处理新生代,存活对象晋升。
- Major GC:较慢、全面,使用标记-清除/压缩处理老生代,避免碎片。
- 协同核心:
- 代际分离:适应对象生命周期差异。
- 写屏障:维护跨代引用正确性。
- 晋升机制:连接两代回收。
- 优化目标:减少主线程停顿,提高应用响应速度。
通过这种分代与协同设计,V8 在内存回收效率和应用性能之间取得了平衡。理解这一机制有助于编写内存友好的 JavaScript 代码,并能在性能调优时预判垃圾回收行为。