Python不回收含__del__的循环引用对象,因析构顺序无法安全确定,导致其滞留gc.garbage中引发内存泄漏;weakref可破环,但需避免与__del__共存。

为什么 __del__ 无法解决循环引用的内存泄漏
Python 的垃圾回收器(GC)能处理大部分循环引用,但前提是对象**没有定义 __del__ 方法**。一旦类中实现了 __del__,GC 就会将该对象放入“不可达但带终结器”的特殊队列,不再自动清理——哪怕所有外部引用都已消失,对象也会一直卡在 gc.garbage 里。
- 典型现象:
gc.collect()返回值为 0,但len(gc.garbage)持续增长 - 调试方法:启用
gc.set_debug(gc.DEBUG_UNCOLLECTABLE),运行后看终端是否打印 “uncollectable” 对象信息 - 根本原因:带
__del__的循环引用会阻断 GC 的可达性分析路径,形成“悬挂终结器”
用 weakref 打破父-子引用链
最常见的循环引用发生在父子关系中:父对象持有一个子对象列表,而子对象又通过 parent 属性反向引用父对象。此时应让子对象用 weakref.ref 存储父引用,而非强引用。
- 不推荐:
self.parent = parent(强引用 → 循环) - 推荐:
import weakref; self._parent_ref = weakref.ref(parent) - 访问时需判空:
parent = self._parent_ref(); if parent is not None: ... - 注意:
weakref.ref返回的是可调用对象,不是直接值;不能对它做is或==判断,必须先调用
避免在闭包或回调中隐式捕获实例
类方法被传作回调(如事件注册、线程 target、functools.partial)时,容易无意中把整个实例带上——尤其当回调函数需要访问 self 的属性或方法时。
- 错误示例:
threading.Timer(1, self.on_timeout).start()→self被强持有 - 修复方式:改用静态方法 + 显式传参,或用
weakref.WeakMethod(Python 3.4+):cb = weakref.WeakMethod(self.on_timeout); timer = threading.Timer(1, lambda: cb()()) - 更稳妥做法:在类销毁前主动注销回调,比如在
__del__或上下文管理器__exit__中调用unregister()
检查第三方库是否引入隐式循环引用
像 logging、asyncio、sqlalchemy 等库在内部可能持有对 handler、loop、session 的长生命周期引用,若你把实例绑到它们身上(比如 logger.addHandler(MyHandler(self))),就可能触发泄漏。
立即学习“Python免费学习笔记(深入)”;
- 排查命令:
gc.get_referrers(obj)查看谁在引用该对象(注意结果可能包含帧对象,需过滤) - 关键点:不是所有循环引用都会泄漏,只有涉及 GC 不可达区域 + 终结器 + 长生命周期容器时才真正危险
- 真实陷阱:
__slots__能减少内存占用,但**不能解决循环引用问题**;__weakref__是必需的,否则weakref.ref会失败


















