引用计数无法处理循环引用,因循环引用使引用数恒≥1;gc模块通过标记-清除补漏,从根对象出发标记可达对象,清理不可达循环;含__del__的循环引用会进入gc.garbage需手动处理。

引用计数无法处理循环引用
Python 默认靠 sys.getrefcount() 背后的引用计数机制回收对象:只要引用数归零,立刻释放。但循环引用会让计数卡在 ≥1——比如 a 持有 b,b 又持有 a,即使外部所有变量都 del 了,它们互相“撑着”对方的计数,谁都不为 0。
这不是 bug,是设计使然:引用计数必须快、确定、无暂停。它不追踪对象图结构,只看“有几个地方指着我”,没法发现闭环。
gc 模块用标记-清除解决不可达循环
gc 模块不替代引用计数,而是补漏:它定期从根对象(如全局变量、栈帧局部变量)出发,遍历所有能到达的对象,标记为“存活”;剩下的就是不可达孤岛——包括那些卡住的循环引用组,然后统一清理。
这个过程需要暂停程序(Stop-The-World),所以不能每创建一个对象就跑一遍。它靠代际阈值驱动:gc.get_threshold() 返回的 (700, 10, 10) 意味着第 0 代每新增约 700 个对象才触发一次轻量扫描。
立即学习“Python免费学习笔记(深入)”;
- 手动调用
gc.collect(0)可立即清第 0 代,适合刚拆完大循环结构后 - 调用
gc.collect()清全代,开销更大,但能扫到滞留在第 2 代的深层循环 - 若代码 100% 确认无循环引用(如纯 NumPy 数值计算脚本),可
gc.disable()省点 CPU
带 __del__ 的循环引用会进 gc.garbage
一旦循环里的任意对象定义了 __del__ 方法,gc 就不敢自动回收——因为无法安全决定析构顺序,怕引发未定义行为或异常。这些对象会被移到 gc.garbage 列表里,一直占内存,直到你手动清空它或重启进程。
更麻烦的是:__del__ 里如果抛异常,Python 只发 ResourceWarning,不中断流程,问题容易被忽略。
- 避免在
__del__中做任何可能失败的操作(如网络请求、文件写入) - 不要让
__del__引用新对象,否则可能延长生命周期甚至制造新循环 - 调试时可用
gc.set_debug(gc.DEBUG_UNCOLLECTABLE)查看哪些对象被塞进了gc.garbage
哪些对象根本不在 gc 扫描范围内
gc 只管“可收集对象”:容器类型(list、dict、自定义类实例)和带 __del__ 的对象。以下几类它完全不管:
-
int、str、tuple这类原子类型——全靠引用计数,永远没循环问题 - NumPy 数组等 C 扩展对象——除非作者正确实现了
tp_traverse和tp_clear,否则逃逸检测 - 被模块级字典、线程局部存储(
threading.local())、或长期存活的闭包持有的对象——逻辑上已废弃,但 GC 看它还“活得好好的”
这也是为什么有时 gc.collect() 返回数字却不降内存:不是 GC 失效,而是对象还被某个你没意识到的地方强引用着。


















