引用计数为 0 就立刻释放——这是 CPython 的主要回收机制。但两个对象互相引用时,计数永远不会归零。这篇把「环」怎么产生、gc 怎么发现它、我自己写一遍这套算法能不能得到同样的答案,都真实跑了一遍。
实验 gc.collect(); gc.disable() a, b = N("a"), N("b") a.refs.append(b); b.refs.append(a) # 互相引用 wa = weakref.ref(a) del a, b # 名字没了, 但它们还互相指着
a、b 互相引用。del a, b 之前 sys.getrefcount(a) = 3 (a 变量 + b.refs 里的一个 + getrefcount 的实参) del a, b 之后, 弱引用还活着吗? True → 引用计数没有归零,对象泄漏在堆里 gc.collect() 返回(回收的对象数): 4 | 之后弱引用还活着吗? False
del a, b 之后没有任何变量指向它们,可弱引用显示对象还在——因为 a 被 b 引用、b 被 a 引用,计数各剩 1,永远不会归零。gc.collect() 之前,它们就泄漏在堆里。回收数是 4:a、b 以及它们各自的 refs 列表(列表也是对象)。
gc.get_threshold() = (2000, 10, 10) | 分代数: 3 | gc.isenabled() = False
gc.is_tracked(整数 1 ) = False
gc.is_tracked(字符串 'x' ) = False
gc.is_tracked(空列表 [] ) = True
gc.is_tracked(空字典 {} ) = True
gc.is_tracked({'a': 1} ) = True
gc.is_tracked({'a': []} ) = True
gc.is_tracked(元组 (1, 2) ) = False
gc.is_tracked(元组 (1, []) ) = True
gc.is_tracked(自定义对象 N() ) = True
gc.is_tracked(N 的类 ) = Truegc 阈值是 (2000, 10, 10),共 3 代。整数、字符串这类不能引用别的对象的东西不需要被追踪(is_tracked 为 False);列表、字典、自定义对象、类都是被追踪的。元组比较特殊:(1, 2) 没被追踪,(1, []) 被追踪了——因为它里面有一个可变的、能引用别人的对象。这里只列观察到的结果,具体怎样触发「取消追踪」的规则我没有逐条验证。
(校准: 只被一个全局名字引用的对象 P, getrefcount 原始值 = 2, 所以观察本身带来 1 个额外引用。下表都已减掉) 节点 引用计数 减去「图内部的引用」之后(gc_refs) A 1 0 B 1 0 C 2 1 D 1 0 E 1 0 gc_refs > 0 的是外部可达的根: ['C'] → 从根出发能到达: ['C', 'D'] 我的算法判定的垃圾: ['A', 'B', 'E'] gc.collect() 之前 __del__ 已调用的: [] gc.collect() 回收对象数: 3 | __del__ 通知到的: ['A', 'B', 'E'] | 一致? True | 全局 C 还活着: C D
思路是:每个对象的引用计数里,有一部分是「这一批对象互相引用」产生的。把这一部分减掉,剩下的就是「批外的人对它的引用」。剩余 > 0 的是根(有人从外面抓着),根能到达的都活着,其余就是垃圾。校准部分里我用一个只被全局名字引用的对象 P,来扣掉getrefcount观察本身带来的引用。结果:C 是根,D 被 C 引用所以活着,A、B、E 是垃圾,gc.collect() 实际调用 __del__ 的也正好是 A、B、E。
创建 20000 对环形垃圾, gc 自动触发了 38 次回收, 各代次数: {0: 35, 1: 3} | 共回收对象数: 78912
最后 3 次回收: [(1, 2160), (0, 2072), (0, 2072)]
gc.get_stats(): [{'collections': 40, 'collected': 72575, 'uncollectable': 0}, {'collections': 3, 'collected': 6480, 'uncollectable': 0}, {'collections': 3, 'collected': 20, 'uncollectable': 0}]创建 20000 对环形垃圾,期间 gc 自己触发了 38 次回收,绝大多数是第 0 代(新对象),少数升到第 1 代;每次回收都收走了上千个对象。你不需要手动调用 gc.collect(),但如果代码里有大量互相引用的对象,回收发生的时机是由分配数量而不是时间决定的。
gc enabled : 创建 20 万对环形垃圾后, 仍占用 0.10 MB (峰值 0.21 MB) gc disabled : 创建 20 万对环形垃圾后, 仍占用 70.40 MB (峰值 70.40 MB) 之后 gc.collect() 回收: 800000 个对象
同样创建 20 万对环形垃圾:gc 开着,结束时只占 0.10 MB;关掉 gc,占了 70.4 MB,直到手动 gc.collect() 才一次性回收了 80 万个对象。关 gc 是有人会做的优化(比如批处理、fork 之前),但代价是循环引用产生的垃圾不会被回收。
gc、weakref、tracemalloc、sys.getrefcountgc.freeze()、带 __del__ 的对象在旧版本 Python 里的行为