gc.collect()未清理循环引用对象是因为三条件缺一不可:对象必须真正不可达、满足代阈值、且不含__del__方法;分代回收默认只扫第0代,深层循环常滞留第2代;含__del__的循环会被放入gc.garbage永不回收;非容器对象不参与gc。

gc.collect() 调用后对象还在内存里,不是 GC 失效,而是它根本没扫描到——循环引用对象得先“不可达”,再满足代阈值,最后还得避开 __del__,三者缺一不可。
循环引用对象必须真正“不可达”才能被 GC 触及
GC 不清理“还活着”的对象,哪怕它们只靠彼此维系。只要任意一个对象还能从根(如全局变量、栈帧局部变量)被访问到,整个循环就安全存活。
-
a = []; a.append(a)这种自循环,只要a变量名还在作用域,对象就始终可达,gc.collect()返回 0 是正常行为 -
del a后,对象才变成“不可达循环”,但此时 GC 还不一定会动——得等分配数触发阈值,或你手动调gc.collect() - 用
gc.get_referrers(obj)查一次,常会发现你以为删干净了,其实某个日志 handler、模块级字典或第三方库缓存还攥着它
默认只扫第 0 代,深层循环常卡在第 2 代
分代回收不是全量扫描,新对象进第 0 代,活过一轮升第 1 代,再活一轮进第 2 代。循环引用往往跨多轮存活,最终滞留在第 2 代——而 gc.collect() 默认只清第 0 代。
-
gc.get_threshold()返回类似(700, 10, 10):第 0 代每新增 700 个对象触发一次回收;第 1 代需第 0 代回收 10 次才触发;第 2 代又得第 1 代触发 10 次 - 小规模循环(比如只创建几对对象)可能永远达不到第 2 代阈值,长期滞留
- 要强制清掉,得显式调
gc.collect(2),不是gc.collect()
含 __del__ 的循环引用会被 GC 彻底放弃
这不是 bug,是 Python 的保守设计:一旦对象定义了 __del__,且陷入循环,GC 就把它塞进 gc.garbage 列表,再也不碰——因为无法安全决定谁的 __del__ 先执行。
-
gc.collect()无论哪一代,都会跳过gc.garbage中的对象 - 即使你调
gc.collect(2),返回值也不会包含这些对象 - 查
gc.garbage长度不为 0,基本可确认是__del__导致的回收冻结 - 弱引用(
weakref.ref)能破环,但和__del__共存时照样失效——Python 不允许这种组合
非容器对象根本不会进入 GC 扫描范围
CPython 的 GC 只处理容器类型:list、dict、set、类实例。纯数据对象如 int、str、tuple(不含可变元素)压根不进 GC 集合,靠引用计数自己搞定。
- 这意味着
a = (b,); b = (a,)这种元组循环,只要没嵌套容器,GC 根本不看——它连扫描都不扫描 - 真正泄漏高发场景是:
class实例之间互相持有引用,或dict和list套嵌形成闭环 - 用
objgraph.show_growth()或gc.get_objects(generation=2)筛出可疑容器类实例,比瞎猜高效得多
gc.get_referrers(obj),就永远不知道真实引用链在哪。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











