Archipelago

Lec 14垃圾回收 III(GC III)——分代与增量回收

讲义完成更新于 2026-09-16

Lec 14 垃圾回收 III(GC III)——分代与增量回收

接 L13。用分代回收降低回收成本、用增量回收降低停顿,并比较各方案、看实践

TL;DR

  • 分代 GC 利用「大多数对象很快死亡」的弱分代假设,频繁回收小而多垃圾的新生代,少量处理老年代。
  • 老年代到新生代的指针会破坏「只扫新生代」的完整性,需要用写屏障和记忆集记录跨代引用。
  • 增量 GC 把长时间标记或搬迁拆成小步与程序交替执行,降低最长停顿,但需要维护更复杂的三色不变式和屏障。

1. GC 的五个权衡维度(汇总)

定义(评价 GC 的五维)

  • 可达性 (Reachability):精确还是近似(处理环否);

  • 运行期开销 (Runtime overhead):GC 给正常执行增加的成本;

  • 空间效率 (Space efficiency):需要多少额外空间;

  • 回收效率 (Reclamation efficiency):回收每个对象多贵 / 多快;

  • GC 延迟 (Latency):能否避免长停顿。 :::

三种基础方案对照:

方案可达性运行期开销空间回收效率延迟
引用计数近似(漏环)每次指针更新都有开销每对象一个计数计数归零即刻回收无停顿
标记-清扫精确仅分配时(加入链表)标记位 + next 指针单对象低,但需扫遍全部已分配对象可能长停顿
复制式精确(配合标记)仅分配时;分配极快(指针碰撞),压紧改善局部性标记位;但相当于砍半可用空间批量删除;但复制对象可能贵(复制可与标记融合,免重扫)可能长停顿

2. 分代回收(Generational Collectors)

洞察(弱分代假设):多数应用有大量极短命对象与少量极长寿对象。据此把堆按"代 (generation)"分区:

  • 新生代 (young heap):存短命对象,频繁回收,保持较小;

  • 老年代 (old heap):在新生代中历经多轮 GC 仍存活的对象被晋升至此,很少回收。

(可有多于两代。)

为何高效:新生代小,回收快;绝大多数短命对象(如小函数的栈帧、算术中间值)很快死亡、根本不必复制;老年代虽大但极少被遍历/复制。

2.1 关键问题:跨代指针(Cross-Generation Pointers)

定理(老→新指针必须纳入根集)

回收新生代时,我们不想遍历整个老年代(否则失去"小范围快回收"的意义)。但老年代对象可能指向新生代对象——这些从老年代指向新生代的指针,必须被当作根集的一部分,否则会漏标仍可达的新生代对象。

实现上用写屏障 (write barrier) 记录这类跨代指针(见下节,亦称 remembered set)。完整回收 (full collection) 时则像一次(带额外步骤的)标记-清扫,遍历整堆。

定理(分代回收的权衡)

  • 可达性:完整回收精确;仅新生代回收不精确(依赖跨代指针记录)。

  • 运行期开销:需检查是否创建了"指入新生代"的指针;比引用计数略好(只需查指针、不必读旧被指对象)。

  • 空间效率:可用整个堆,开销主要是根集/记录集;若不触发完整回收,可能残留少量垃圾。

  • 回收效率:新生代回收比纯复制式更高效(复制更少);完整回收类似标记-清扫,但理想情况下不必常做。

  • 延迟:新生代回收远好于整堆复制;完整扫描仍是 mark-and-sweep 级别。 :::


3. 增量回收(Incremental Collection)——降低延迟

为避免 GC 的长停顿,把 GC 的执行与变更器 (mutator,即用户程序) 的执行交错 (interleave) 进行。

3.1 三色(对象阶段)

定义(对象的三个标记阶段,即三色标记)

  • unreached(白):尚未被标记到达;

  • unscanned(灰):已到达,但其引用尚未全部跟随完;

  • scanned(黑):已处理完毕。

若 mutator 要与回收交错运行,它必须感知所修改对象的当前阶段。

3.2 危险情形与写屏障

例题(增量回收的漏标 bug)

GC 标记进行到一半时,mutator 插一脚:

1) 把一个指针从【scanned(黑)】对象 → 【unreached(白)】对象(新建);
2) 同时删除【unscanned(灰)】对象到该白对象的原有指针。

结果:GC 以为黑对象已扫完(不会再看它的新指针),而通往白对象的唯一灰路径又被删了 → GC 认为全部扫描完毕,但其实有个可达对象被漏标 → 被错误回收(悬垂指针!)。

定理(写屏障 Write Barrier)

解法:给 mutator 装一个写屏障——在每次写指针时运行的检查。当检测到"从 scanned 对象指向 unscanned/unreached 对象"的新引用时,把被指对象加入待扫描列表。代价:增加运行期开销,换取更低的 GC 延迟。


4. 完整的权衡视角与实践

定义(各技术对应的改进维度)

  • 完整性(环):引用计数 vs. 标记(标记处理环);

  • 空间效率:减少/消除"已分配对象链表"(侵入式头部、复制式);

  • 回收效率:分代回收把搜索大多限制在小空间;

  • 延迟:分代(多数时间只扫小空间)+ 增量(把开销分散到整个运行期)。 :::

::: definition 定义(GC 在实践中)

  • C++:通过 shared_ptr 库(引用计数)。

  • Objective-C / Swift:引用计数(ARC)。

  • Python:引用计数 + 可选的分代回收器(处理环)。

  • Java 11:多种分代回收器——Serial(单线程)、Parallel/吞吐量(多线程加速)、CMS 与 G1(大多并发,与应用并行做昂贵工作)。

延伸阅读:Jones & Lins《Garbage Collection》;Jones, Hosking, Moss《The Garbage Collection Handbook》。


5. 本讲小结

  • 评价 GC 看五维:可达性精确性、运行期开销、空间、回收效率、延迟;引用计数/标记-清扫/复制式各有取舍。
  • 分代回收基于"多数对象短命":新生代小而频繁回收、老年代大而少回收,存活者晋升;新生代回收须把老→新指针纳入根集(写屏障记录)。
  • 增量回收交错 GC 与 mutator 以降延迟;三色(unreached/unscanned/scanned);mutator 可能"黑→白 + 删灰路径"造成漏标,用写屏障把新指针的被指对象补入待扫描列表。
  • 实践:C++ shared_ptr、Swift 引用计数、Python 引用计数 + 分代、Java 多种分代/并发回收器。
  • 至此 Phase 3(GC)完结;下一单元进入底层虚拟机与代码生成(L15 起)。