Python 内存泄漏的定位与解决

1次阅读

用 tracemalloc 可快速定位 python 内存增长源头:需尽早 start(),在稳定复现路径前后 take_snapshot(),再 compare_to() 查增量代码行;注意其不跟踪 c 扩展内存。

Python 内存泄漏的定位与解决

tracemalloc 快速定位内存增长源头

Python 程序跑着跑着变慢、RSS 内存持续上涨,大概率不是 GC 失效,而是对象被意外持有。别急着翻 gc.get_objects(),先用标准库的 tracemalloc——它开销小、精度够、自带调用追踪。

关键不是“开了就看”,而是得在**稳定复现路径前启动、在增长后快照比对**:

  • tracemalloc.start() 要尽早调用,最好在主模块导入后第一行(避免漏掉初始化阶段的分配)
  • 触发疑似泄漏的操作后,用 tracemalloc.take_snapshot() 拍两份快照,再用 snapshot1.compare_to(snapshot2, 'lineno') 查增量最大的 10 行
  • 注意默认只跟踪 Python 分配,C 扩展(如 numpy 数组底层内存)不计入;若怀疑 C 层泄漏,得换 valgrindmemray

gc.garbage 里真有活对象?别信,先确认循环引用是否被打破

看到 gc.garbage 非空,不代表一定泄漏——它只是 GC 暂时无法回收的循环引用集合。很多情况下,这些对象后续会被正常清理,或者根本没逃过 finalizer。

真正要盯的是:这些对象是否在业务逻辑结束后仍长期驻留?检查步骤很直接:

立即学习Python免费学习笔记(深入)”;

  • 手动触发 gc.collect() 后再查 gc.garbage,如果长度不变且对象类型固定(比如全是某个自定义类实例),才值得深挖
  • gc.get_referrers(obj) 查谁还拿着它,重点看是不是被全局 dict、缓存、信号回调、线程局部存储(threading.local)或 Logging handler 意外强引用
  • 常见陷阱:functools.lru_cache 缓存未设 maxsizeweakref.WeakKeyDictionary 错写成普通 dict、用类变量存实例引用

异步任务(asyncio)里忘了 await 或漏了 cancel() 就会吃内存

协程对象本身不占多少内存,但一旦被创建却没被调度或取消,它关联的帧对象(frame)、局部变量、上下文就会一直挂着——尤其在长周期服务中,这种“幽灵 task”越积越多。

排查和防御都得从事件循环入手:

  • asyncio.all_tasks(loop) 列出所有存活 task,过滤掉 done()Falsecancelled()False 的,它们就是可疑分子
  • 启动 task 时务必用 asyncio.create_task() 而非直接调用协程函数;取消时用 task.cancel() + await task,别只 cancel 不 await
  • 日志打点要小心:logging.info(f"start {obj}") 中的 obj 若含大字段或闭包,可能让整个帧无法释放

第三方库的缓存策略常是隐藏泄漏源,尤其 ORM 和 http 客户端

django ORM 的 QuerySetsqlAlchemy 的 session、requests 的连接池、aiohttp 的 ClientSession——它们内部都有缓存或连接保持机制,用错一次,内存就悄悄涨一点。

不能全靠文档,得看实际行为:

  • Django 中避免在循环里反复调用 qs.Filter(...) 却不执行(如不加 list()len()),因为每个 filter 都生成新 QuerySet,而 QuerySet 会缓存 SQL 和结果元信息
  • SQLAlchemy 的 session.query(...).all() 返回的 list 里对象仍被 session 强引用,不用时显式调用 session.expunge_all() 或用 session.close()
  • aiohttp 的 ClientSession 必须配合 async with 使用,手写 session = ClientSession(); await session.get(...) 而不 close,连接和相关缓冲区就一直占着

最麻烦的其实是“看起来没问题”的组合:比如用 lru_cache 包裹一个返回 pandas DataFrame 的函数,DataFrame 本身引用了大量 numpy 数组,缓存键又没控制好粒度——这时候 tracemalloc 显示的往往是 pandas.core.frame.DataFrame,但根因在缓存策略上。

text=ZqhQzanResources