Skip to main content

The Garbage Collection Handbook, Second Edition 中文摘录与读书笔记

原书:The Garbage Collection Handbook: The Art of Automatic Memory Management, Second Edition

作者:Richard Jones、Antony Hosking、Eliot Moss

来源:维护者提供的本地 PDF。

版权说明:本书为受版权保护的商业出版物。本页不提供逐章全文翻译,而以原创的中文导读、概念摘录和学习笔记帮助阅读原书;引文与示例均保持在必要的最小范围内,仅供学习研究使用。

阅读定位

这本书讨论的不是“如何调用某个语言的 GC API”,而是自动内存管理器必须维持的运行时不变量:对象从哪里分配、如何判定可达、何时回收、移动对象后怎样修复引用,以及这些动作如何与并发程序、虚拟内存和硬件缓存共同工作。

推荐先具备下列背景:指针与对象图、栈和堆、基本的操作系统虚拟内存概念,以及至少一种托管运行时或系统语言的使用经验。

核心概念摘录

1. 可达性是回收语义的起点

把寄存器、线程栈、全局变量和运行时句柄表视为根集合。垃圾收集器从根沿引用边遍历;能够到达的对象为存活对象,其余对象才可被回收。这个模型直接导出两个工程问题:根必须完整且精确,指针识别规则必须与语言和 ABI 一致。

2. 分配路径决定了常态性能

很多运行时把分配设计成指针递增:在线性分配区中检查剩余空间,成功后移动分配指针。慢路径才触发扩容、切换分配区或收集。评价收集器时,不能只看一次 GC 停顿,也要看这个常态热路径的指令数、缓存行为和线程竞争。

3. 三类基本回收策略各有交换

策略主要做法优点代价
标记-清扫标记存活对象,再扫描堆回收未标记空间不必移动对象,容易与保守指针识别结合容易产生碎片,分配器更复杂
标记-整理标记后压缩存活对象消除碎片,提高局部性需要更新引用,移动成本较高
复制将存活对象复制到新空间分配极快,天然完成整理需要预留复制空间,存活率高时复制成本大

选择不能脱离工作负载:对象寿命分布、峰值内存、地址稳定性需求和停顿预算都会改变答案。

4. 分代假设把“多数对象早死”变成优化

分代收集让年轻对象在小而频繁的收集周期中处理,让长期存活对象较少参与。代际之间的引用是正确性的关键:当老对象指向年轻对象时,收集年轻代不能漏掉这条边。写屏障与记忆集正是为此服务;它们把部分写入成本换成更小的扫描范围。

5. 并发收集的难点是保持快照语义

应用线程和收集线程同时读写对象图时,单次遍历并不能自动代表某一时刻的堆。增量更新与快照开始时两类屏障协议,分别规定并发写入时应记录什么。理解屏障时,应先写出不变量,再分析每个读写位置如何维护它;先背术语通常会迷失在实现细节里。

6. 引用计数不是“没有暂停的 GC”

引用计数能在计数归零时立即释放对象,但每次引用更新都可能产生维护开销,并且环状结构需要额外的循环检测或弱引用机制。追踪式收集对循环天然友好,却要在收集时遍历对象图。现代运行时常常组合两者,而不是把它们视为非此即彼的选择。

阅读路线

  1. 先建立对象图、根集合、精确与保守扫描的共同语言。
  2. 依次比较标记-清扫、整理和复制算法,记录它们对碎片与指针稳定性的要求。
  3. 进入分代、写屏障和记忆集;用“老对象写入年轻对象”手工推演一次小型收集。
  4. 最后阅读并发、实时和大堆场景,重点关注不变量、屏障开销和尾延迟。

实践检查表

  • 为运行时记录对象布局、指针位图和根枚举策略,而不是让这些规则散落在代码中。
  • 分别测量分配吞吐、平均停顿、P99 停顿、峰值驻留集和碎片率。
  • 为所有跨代写入建立可验证的屏障路径,并测试老对象到年轻对象的引用。
  • 移动对象前确认 FFI、JIT 代码、句柄和调试器是否保存了裸地址。
  • 并发收集器先以明确的不变量和压力测试证明正确性,再谈吞吐优化。

延伸问题

  • 你的对象模型能否精确区分指针与非指针字段?
  • 工作负载的对象寿命是否真的符合“多数对象早死”的假设?
  • 停顿预算是由交互帧、服务尾延迟还是批处理吞吐决定的?
  • 当堆变大而存活集不变时,算法的扫描成本如何变化?