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,也不会消除保留路径。它可能推迟容量不足暴露的时间,并挤占容器中堆外内存的余量;应同时观察可达数据量和总内存。
Reference counting 把成本放在引用更新
基础引用计数模型在强引用增减时维护计数,并在归零时触发释放;具体实现可能延迟计数更新或回收。优点是回收时机较可预测,缺点包括更新开销、多线程原子操作和 cycle:A 引用 B、B 引用 A,即使外部不可达计数仍不为零。
CPython 把引用计数与循环垃圾收集结合;其他 Python 实现未必使用相同策略。Swift ARC 和 Rust Rc 可用 weak reference 打破所有权环。不是所有引用计数都“没有停顿”,cycle collection 和析构级联仍可能集中产生工作。
Ownership 把释放责任放进静态规则
Rust 用所有权安排值的销毁:通常在所有者作用域结束时执行 drop glue,并调用适用的 Drop 实现;borrow checker 约束引用的有效期与可变别名。这些规则让 safe Rust 无需 tracing GC 也能防止 use-after-free。
析构不是无条件的资源释放保证:mem::forget、进程退出或 abort 等情况可以跳过它。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。
下面的 CPython 3 示例不靠等待猜回收时机。保存为 gc_demo.py 后运行 python3 gc_demo.py;它暂时关闭自动循环收集,再显式触发收集:
import gc
import weakref
class Node:
pass
was_enabled = gc.isenabled()
gc.disable()
try:
node = Node()
node.link = node
cache = {"held": node}
observed = weakref.ref(node)
del node
gc.collect()
print(observed() is not None)
cache.clear()
gc.collect()
print(observed() is None)
finally:
if was_enabled:
gc.enable()输出两行 True。第一次收集后,cache 仍保留对象;清空 cache 后,只剩不可达的自引用环,下一次收集将它释放。弱引用没有延长对象寿命。GC 已经认真清理了,缓存还抓着对象不放,就不能继续怪扫地的人。
非内存资源需要显式作用域
文件、socket、数据库事务和锁不能等待 GC。使用 RAII、try-with-resources、context manager 或 defer,让释放与词法作用域绑定,并定义异常/取消路径。
Finalizer/Cleaner 只能作为兜底,执行时机不确定,进程退出前可能根本不运行。高负载下依赖 finalization 关闭连接会先耗尽 fd。
下一课处理多线程下的另一层“内存模型”:何时一个线程的写入必须被另一个线程观察到。