gc.get_referrers() 是定位python内存泄露的关键工具,它能直接找出持有对象引用的容器,而非盲目追踪被引用对象;配合 objgraph 可视化引用路径,结合检查 __del__、weakref、lru_cache 等常见陷阱,可高效解决循环引用导致的 gc 失效问题。

用 gc.get_referrers() 找出谁在持有着对象
循环引用本身不会阻止 Python 垃圾回收(CPython 的循环检测器能处理大部分情况),但一旦对象定义了 __del__ 方法,或被 weakref、functools.lru_cache、某些 C 扩展(如 numpy 数组的某些 view)间接持有,GC 就会跳过它——这时内存才真正“泄露”。定位的关键不是看“谁被引用”,而是看“谁在引用它”。gc.get_referrers(obj) 能列出所有直接持有该对象引用的容器,比盲目查 gc.get_referents() 有效得多。
实操建议:
- 先触发疑似泄露场景(比如反复创建某类实例后不释放),再调用
gc.collect()确保 GC 已运行 - 用
gc.get_objects()拿到疑似残留对象(例如数量异常增长的MyClass实例),挑一个样本obj - 逐层调用
gc.get_referrers(obj),重点关注字典、列表、模块级变量、闭包的__closure__、类的__dict__—— 这些是最常见的“意外持有者” - 若返回结果里出现
module或function,检查是否模块级缓存没清空,或装饰器(如@lru_cache)误缓存了带状态的对象
检查 __del__ 和弱引用混用导致的 GC 放弃
只要任意一个参与循环的对象定义了 __del__,整个循环就无法被 GC 自动回收。更隐蔽的是:你可能没写 __del__,但用了 weakref.ref 或 weakref.WeakKeyDictionary,而它们内部会注册回调函数,这些回调常隐式捕获对象形成新引用链。
实操建议:
- 搜索代码中所有
def __del__(self):,临时注释掉,再测内存是否回落——这是最快验证方式 - 避免在
__del__中访问其他可能已被销毁的对象(如全局字典、日志器),否则易引发异常并中断 GC 清理流程 - 用
weakref.ref(obj, callback)时,确保callback是纯函数,不闭包持有obj或其属性;优先用weakref.finalize(obj, func)替代手动管理 - 检查第三方库(如
requests.adapters.HTTPAdapter的连接池)是否内部使用了弱引用 + 回调,升级到较新版本往往已修复
用 objgraph 可视化引用路径(需安装)
objgraph 不是标准库,但它能快速生成 PNG 引用图,对复杂嵌套结构特别管用。比起手动层层 get_referrers,它直接标出从根对象(如 module)到目标对象的最短强引用路径,一眼看出哪条链本该断开。
实操建议:
- 安装:
pip install objgraph;启动前加import objgraph; objgraph.show_growth()监控对象增长 - 泄露复现后,运行
objgraph.show_backrefs([obj], max_depth=5, too_many=10),重点看输出中以module、function、dict开头的路径 - 若图中出现
functools._lru_cache_wrapper或threading.local,说明缓存或线程局部存储未清理,对应检查cache_clear()调用时机 - 注意:
objgraph会增加内存开销,生产环境慎用;调试完记得卸载或禁用相关 import
避免在闭包和装饰器里隐式持有实例
装饰器返回闭包、事件回调绑定方法、lambda 捕获 self——这些都极易制造单向强引用,与对象自身的属性(如回调列表)构成循环。问题在于:开发者常以为“只传了函数名”,实际传的是绑定了 self 的方法对象,它自带对实例的强引用。
实操建议:
- 把回调注册逻辑从
self.callback = self.handle_event改为self.callback = lambda *a, **kw: self.handle_event(*a, **kw)是错的,这反而强化了引用;正确做法是用functools.partial或显式传参 - 装饰器中若要缓存实例方法,必须用
weakref.WeakKeyDictionary存键,并在方法执行前后确认 key 是否仍有效 - GUI 或异步框架(如
asyncio、PyQt)中,用QObject.deleteLater()或asyncio.create_task()后忘记cancel(),会导致任务对象持续持有回调引用 - 日志记录器(
logging.getLogger(__name__))本身不泄露,但若在实例方法里用self.logger = logging.getLogger(f"{__name__}.{id(self)}"),就人为制造了模块级 logger 引用
循环引用的麻烦不在“有”,而在“谁不让 GC 动手”。多数时候,删掉一个 __del__、清空一个模块变量、或把闭包改成弱回调,就能让内存立刻回落。别花时间写复杂的引用计数工具,先盯住那几个高频嫌疑点。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











