2.2 内存管理:Tracing GC、引用计数与所有权
内存管理策略回答两个问题:对象何时不再可达或不再拥有,以及回收成本由谁在何时支付。自动管理减少一类悬垂指针,不会自动消除资源泄漏、延迟尖峰或错误生命周期。
Tracing GC 从 Root 计算可达性
roots: stacks, static fields, runtime handles
→ trace object graph
→ reachable live set
→ reclaim unreachable objectsMark-sweep 标记后回收空闲块;mark-compact 还移动存活对象减少碎片;copying collector 把存活对象复制到另一空间。真实收集器会组合并并发执行这些阶段。
Generational hypothesis 观察多数对象寿命短,于是年轻代频繁、小范围回收,长寿对象晋升。跨代引用需要 write barrier/card table 等记忆集,不能只扫描年轻代就忘记老对象指向它。
Pause、Throughput 与 Footprint 相互制约
并发 GC 把部分工作与应用线程重叠,降低 pause,却消耗 CPU、需要 barrier,并可能要求更大 heap。低延迟 collector 也有 safepoint、root 扫描和退化情形,不等于“绝不暂停”。
调优先看:allocation rate、live set、promotion、pause distribution、concurrent cycle 是否赶上、heap headroom 与容器实际内存。只把最大 heap 调大可能延后问题,也可能增加 live set 扫描与 OOM 故障半径。
Reference counting 把成本放在引用更新
每次强引用增减维护计数,归零立即释放。优点是回收时机较可预测,缺点包括更新开销、多线程原子操作和 cycle:A 引用 B、B 引用 A,即使外部不可达计数仍不为零。
Python 等实现可用 cycle detector 补充,Swift/Rust Rc 常通过 weak reference 打破所有权环。不是所有引用计数都“没有停顿”,cycle collection 和析构级联仍可能集中产生工作。
Ownership 把释放责任放进静态规则
Rust 每个值有 owner,owner 离开作用域时运行 Drop;borrow checker 保证引用不超过被借用值的合法生命周期,并限制可变别名。它不需要 tracing GC 才能保证普通安全代码无 use-after-free。
但 Rc cycle 仍可泄漏,unsafe/FFI 仍需人工证明,arena 中对象可能一直活到整区释放。Ownership 不是“所有东西必在栈上”,Box、Vec、Arc 都会使用 heap。
GC 语言也会泄漏
只要对象仍从 root 可达,GC 就不能判断业务已不用它:无界 cache、listener 未注销、ThreadLocal、ClassLoader、队列积压和错误 metrics label 都会保留对象。
排查区分 allocation hot spot 与 retention path。Heap dump 的 dominator tree 告诉哪些对象保留大量内存;只按对象数量找“最大的类”常会错过真正 root。
非内存资源需要显式作用域
文件、socket、数据库事务和锁不能等待 GC。使用 RAII、try-with-resources、context manager 或 defer,让释放与词法作用域绑定,并定义异常/取消路径。
Finalizer/Cleaner 只能作为兜底,执行时机不确定,进程退出前可能根本不运行。高负载下依赖 finalization 关闭连接会先耗尽 fd。
下一课处理多线程下的另一层“内存模型”:何时一个线程的写入必须被另一个线程观察到。