Python 的垃圾回收到底回收了谁?引用计数+分代回收一次讲清
Python 的垃圾回收到底回收了谁引用计数分代回收一次讲清系列Python 底层设计逻辑类型原理拆解型分发CSDN / 博客园 / 腾讯云字数约 1600 字结论先行CPython 的垃圾回收主力是引用计数——每个对象头里存着一个计数引用清零的瞬间对象立即被释放实时性拉满。但引用计数有一个致命软肋循环引用A 指着 B、B 指着 A计数永远降不到 0。为了兜这个底CPython 又叠加了第二套机制分代回收generational GC——定期扫描容器对象专门清理计数不为 0 但已经没人要的循环引用垃圾。两套机制配合才有 Python 基本不用管内存的体验。1. 主力机制引用计数清零即释放CPython 里每个对象都有一个引用计数ob_refcnt。每多一个名字指向它计数 1每少一个计数 -1。计数归零的瞬间对象立即释放——不需要等待不需要扫描。importsys a[1,2,3]print(sys.getrefcount(a))# 2变量 a getrefcount 的临时参数baprint(sys.getrefcount(a))# 3多了个 bdelbprint(sys.getrefcount(a))# 又回到 2注意 getrefcount 的结果永远比肉眼多 1——因为调用它时实参本身也算一次临时引用。这个小细节经常在面试里被追问。引用计数的优点是实时性对象一死立刻回收内存曲线平滑不会像某些语言一样攒一波再卡一下。代价是每个对象都要维护计数且每次赋值都要改计数——速度上的开销换来了行为的可预测。2. 致命软肋循环引用classNode:def__init__(self):self.partnerNoneaNode()bNode()a.partnerb b.partneradeladelb# 两个对象的计数都还是 1谁都释放不了a和b互相指着对方del之后计数各剩 1永远降不到 0。如果只靠引用计数这段代码每跑一次就漏一块内存——跑个几天几夜服务就 OOM 了。这类计数不为 0 但其实已经没人要的垃圾只能靠第二套机制兜底。3. 兜底机制分代回收CPython 的分代回收基于一个统计学假设大部分对象朝生夕死活得越久的对象越不可能变成垃圾。它把所有容器对象分成三代第 0 代最年轻每代是一个链表代绰号触发条件第 0 代年轻代新建对象数 − 删除对象数 超过阈值 700第 1 代中年代第 0 代回收次数满 10 次第 2 代老年代第 1 代回收次数满 10 次触发时做什么从该代开始遍历容器对象把互相引用但外部已不可达的对象标记为垃圾一起清掉。第 2 代回收full collection最重会把所有代都扫一遍。用gc模块可以亲眼看到这套节奏importgcprint(gc.get_count())# (当前0代计数, 1代计数, 2代计数)print(gc.get_threshold())# (700, 10, 10)——三代阈值gc.collect()# 手动触发一次 full collectionprint(gc.get_stats())# 各代历次回收的统计4. 手动 gc 管不管用分场景循环引用密集的场景比如大量创建互相引用的图结构、解析器 AST、带环的配置对象手动gc.collect()能立刻把循环垃圾收掉适合在一段密集计算结束后调用。普通业务代码别瞎调。分代回收本身开销可控手动 full collection 反而会造成一次明显的停顿。追求低延迟的场景Web 请求、游戏循环可以调阈值比如gc.freeze()把老年代对象冻结减少每次 full GC 的扫描量——但这属于进阶操作改之前先压测。一个反直觉的事实gc.disable()能让吞吐更高但循环引用会无限堆积内存只涨不跌。除非你确定程序里根本没有循环引用否则别关。5. 常见误区澄清误区一del x之后对象就立刻释放了。不一定。del只是把x这个标签撕了计数减 1。如果还有别的引用比如列表元素、闭包捕获、异常 traceback对象照样活着。计数归零才释放del不保证归零。误区二引用计数是纯 Python实现的。不是。它在 C 层由解释器在对象创建/赋值/传参时自动维护Python 代码碰不到也关不掉能关的只是分代回收那套。误区三Python 有 GIL所以 GC 是线程安全的。分代回收确实是解释器统一调度的但gc.collect()在 C 层跑的时候会持有 GIL长回收会阻塞所有线程——这也是手动 full GC 造成卡顿的根源。6. 对比其他实现怎么玩实现主力回收机制特点CPython引用计数 分代回收实时释放循环引用靠兜底扫描PyPy增量式标记清除无引用计数靠 GC 统一调度延迟低Jython / IronPython借宿主机 JVM / .NET GC交给虚拟机的成熟 GC理解这个对比的意义引用计数不是 Python 语言的规定而是 CPython 这个参考实现的选择。写代码时你可以依赖对象没人要就会被收走但不要依赖什么时候精确收走——那是实现细节。7. 一句话总结主力引用计数清零即释放实时但怕循环引用兜底分代回收0/1/2 代阈值 700/10/10定期扫容器专杀循环垃圾实践循环引用密集处才手动gc.collect()del不保证释放别随便gc.disable()。想清楚这两套机制怎么配合你看 Python 内存曲线时的每一个疑问就都有了解释。下一篇《Python 的浅拷贝和深拷贝到底差在哪一个嵌套列表实验讲透》——copy()之后改一个为什么另一个也跟着变。关注不迷路。