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”
引用计数能在计数归零时立即释放对象,但每次引用更新都可能产生维护开销,并且环状结构需要额外的循环检测或弱引用机制。追踪式收集对循环天然友好,却要在收集时遍历对象图。现代运行时常常组合两者,而不是把它们视为非此即彼的选择。
阅读路线
- 先建立对象图、根集合、精确与保守扫描的共同语言。
- 依次比较标记-清扫、整理和复制算法,记录它们对碎片与指针稳定性的要求。
- 进入分代、写屏障和记忆集;用“老对象写入年轻对象”手工推演一次小型收集。
- 最后阅读并发、实时和大堆场景,重点关注不变量、屏障开销和尾延迟。
实践检查表
- 为运行时记录对象布局、指针位图和根枚举策略,而不是让这些规则散落在代码中。
- 分别测量分配吞吐、平均停顿、P99 停顿、峰值驻留集和碎片率。
- 为所有跨代写入建立可验证的屏障路径,并测试老对象到年轻对象的引用。
- 移动对象前确认 FFI、JIT 代码、句柄和调试器是否保存了裸地址。
- 并发收集器先以明确的不变量和压力测试证明正确性,再谈吞吐优化。
延伸问题
- 你的对象模型能否精确区分指针与非指针字段?
- 工作负载的对象寿命是否真的符合“多数对象早死”的假设?
- 停顿预算是由交互帧、服务尾延迟还是批处理吞吐决定的?
- 当堆变大而存活集不变时,算法的扫描成本如何变化?