人人都会AI编程

新生代:Scavenge 算法、对象晋升

更新时间:2026-07-11

V8 引擎将堆内存划分为两个主要区域:新生代(Young Generation)老生代(Old Generation)。新生代用于存放存活时间短、朝生夕死的对象(如局部变量、临时计算结果),老生代则存放那些经历了多次垃圾回收依然存活、生命周期较长的对象(如全局对象、闭包引用、长期存在的缓存)。

空间划分与 Scavenge 算法

新生代内部被切分为两个大小相等的半空间:From 空间To 空间。对象总是在 From 空间中分配,To 空间则始终保持空闲。

当新生代空间即将占满时,V8 会触发一次 Scavenge 垃圾回收,它的核心是复制-清空算法,流程大致如下:

  1. 标记:从根集合(全局对象、调用栈上的变量等)出发,标记 From 空间中所有可达(存活)的对象。
  2. 复制:将标记为存活的对象从 From 空间逐个复制到 To 空间中,并在复制时紧凑排列,消除内存碎片。
  3. 角色互换:复制完成后,清空原有 From 空间,然后将 From 空间和 To 空间的角色对调——原来被清空的 From 空间变为新的 To 空间,等待下一次回收;而存放了存活对象的原 To 空间则变为新的 From 空间,程序继续在其中分配新对象。

这样,只有存活的对象才会被复制和移动,那些已经死亡的对象就直接留在 From 空间中被整体清除,回收过程本身几乎不遍历死亡对象,因此非常高效。这也是“牺牲空间换时间”的设计:用一半的内存空间空闲来换取极快的回收速度。

对象晋升(Promotion)

新生代中的对象并非永远停留在新生代。当一个对象在 Scavenge 过程中满足以下两个条件之一时,会被晋升(Promotion)到老生代

  1. 经历过两次 Scavenge 回收仍存活

每次 Scavenge 将对象从 From 复制到 To 时,都会给对象增加一个隐藏的存活计数器。如果一个对象在 From → To 的复制过程中已经经历了一次回收,下一次再被复制时,它的计数器就达到了阈值,引擎便不再将它放入新的 To 空间,而是直接拷贝到老生代中。这种设计基于“存活越久的对象,越可能继续存活”的假设(即分代假说)。

  1. To 空间使用率超过 25%

如果当前 From 空间中存活对象复制到 To 空间后,To 空间的使用率已经超过了 25%,为了留出足够的空闲空间给后续的新对象分配,引擎会将后续的存活对象直接晋升到老生代,而不是继续塞进 To 空间。这个阈值的设定可以保证新生代有充足的空闲区,避免频繁触发垃圾回收。

对象晋升是老生代对象的主要来源。当老生代也接近容量极限时,就会触发老生代的回收算法(标记清除、标记整理等),这些将在下一小节中详细展开。

实际影响与注意事项

从开发者的角度看,理解新生代的回收机制有助于优化内存使用:

  • 避免频繁创建大量短生命周期的大对象:如果一个对象很大,却在新生代早期就被复制复制,复制成本较高,且可能因为体积过大直接进入老生代(大对象往往直接在老生代分配),这会增加老生代的回收压力。
  • 注意函数作用域中的闭包引用:闭包会让本应在作用域结束后就被回收的局部变量“意外存活”,如果大量这样的变量被创建且被闭包长期持有,它们可能经过数次 Scavenge 后晋升到老生代,长期占用内存,这种情况容易导致内存泄漏。
  • Scavenge 过程是“Stop-The-World”暂停:虽然新生代回收速度很快(通常只耗时几毫秒),但在高频异步操作中仍可能产生可感知的卡顿。合理控制对象创建速率,可以减少回收频率,提升交互流畅度。

简言之,新生代的 Scavenge 回收为 JavaScript 内存管理提供了一个高效的“第一道防线”,而对象晋升机制则自然地将长寿命对象过渡到管理成本更低的老生代,两者协作构建了 V8 的分代回收体系。