在 V8 的内存管理中,老生代(Old Generation)负责存储那些经过新生代多次回收后依然存活的对象。与新生代相比,老生代的对象有三个明显特征:存活时间长、空间容量大、分配和回收频率低。因此,老生代不能像新生代那样频繁使用 Scavenge 算法(复制算法),因为复制一个大对象群的成本太高,也浪费一半空间。取而代之的是两种经典回收策略:标记清除和标记整理。
标记清除(Mark-Sweep)
这是老生代最核心的垃圾回收算法,分为两个阶段:
- 标记阶段
垃圾回收器从一组“根对象”(Roots)出发,遍历所有能访问到的对象,将其标记为“存活”。这里的根对象包括全局对象(window / global)、当前调用栈中的局部变量、闭包引用的变量等。所有从根可达的对象都会被标记,未被标记的对象则被视为“垃圾”。
- 清除阶段
回收器线性遍历整个老生代的内存空间,将未被标记的对象回收,其占用的内存回归空闲列表(Free List),供后续分配使用。标记存活对象本身不会被移动,只是把垃圾空间重新标记为可用。
实用特点与代价
标记清除算法简单、有效,能够处理循环引用等复杂对象关系。但它有一个显著的副作用:内存碎片化。由于清除时只回收垃圾对象的空间,而存活对象原地不动,长时间运行后,内存中会出现大量不连续的空闲空间。碎片会导致两个问题:一是大对象分配可能找不到连续空间而触发更早的垃圾回收;二是内存利用率降低。
标记整理(Mark-Compact)
为了解决标记清除带来的碎片问题,老生代在必要时会执行标记整理。它在标记阶段与标记清除完全一致,区别在于清理方式:清除变成了整理——将所有存活对象向内存空间的一端移动,移动结束后,存活对象被紧密排列在一起,边界之外的内存一次性全部释放。
整理前: [存活A][垃圾][存活B][垃圾][存活C]
整理后: [存活A][存活B][存活C][_________空闲_________]
实用特点与代价
标记整理消灭了内存碎片,让后续分配可以快速找到连续空间。但代价也很明显:移动对象需要更新所有指向它的引用,这是一项耗时操作,比简单的清除要慢得多。因此,V8 不会在每次老生代回收时都执行整理,而是采用一种折中策略:
- 常规回收时使用标记清除,快速释放垃圾,容忍一定碎片。
- 当碎片化严重到无法为新对象分配空间、或垃圾回收完成后剩余空闲空间不足、或即将进行新生代晋升时,才会触发标记整理来压缩内存。
这种“清除为主、整理为辅”的组合,既保证了回收速度,又在必要时兜底碎片问题,是经过大量实践验证的平衡方案。
实际表现与开发者关注点
对于开发者而言,老生代的垃圾回收通常不会成为性能瓶颈——因为它的触发频率远低于新生代。但如果应用长时间不刷新、不断分配新对象并长期持有引用,老生代内存会持续增长,最终触发 Full GC(同时回收新生代和老生代),导致明显停顿。在性能监控中,如果发现内存曲线持续上升且频繁出现“Major GC”停顿,往往意味着存在未及时清理的大对象引用或意外全局变量。
此外,理解老生代回收机制有助于解释一些常见的内存优化建议:尽量让对象“短命”(在新生代就被回收),及时断开不用的引用(赋值为 null),避免在长生命周期对象中意外持有大量短期对象(如事件监听器未解绑、闭包引用过大的环境变量)。这些都是为了让老生代保持“轻量”,避免碎片积累和 Full GC 峰值。