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

用 tracemalloc 快速定位内存增长源头
Python 程序跑着跑着变慢、RSS 内存持续上涨,大概率不是 GC 失效,而是对象被意外持有。别急着翻 gc.get_objects(),先用标准库的 tracemalloc——它开销小、精度够、自带调用栈追踪。
关键不是“开了就看”,而是得在**稳定复现路径前启动、在增长后快照比对**:
-
tracemalloc.start()要尽早调用,最好在主模块导入后第一行(避免漏掉初始化阶段的分配) - 触发疑似泄漏的操作后,用
tracemalloc.take_snapshot()拍两份快照,再用snapshot1.compare_to(snapshot2, 'lineno')查增量最大的 10 行 - 注意默认只跟踪 Python 分配,C 扩展(如 numpy 数组底层内存)不计入;若怀疑 C 层泄漏,得换
valgrind或memray
gc.garbage 里真有活对象?别信,先确认循环引用是否被打破
看到 gc.garbage 非空,不代表一定泄漏——它只是 GC 暂时无法回收的循环引用集合。很多情况下,这些对象后续会被正常清理,或者根本没逃过 finalizer。
真正要盯的是:这些对象是否在业务逻辑结束后仍长期驻留?检查步骤很直接:
立即学习“Python免费学习笔记(深入)”;
- 手动触发
gc.collect()后再查gc.garbage,如果长度不变且对象类型固定(比如全是某个自定义类实例),才值得深挖 - 用
gc.get_referrers(obj)查谁还拿着它,重点看是不是被全局 dict、缓存、信号回调、线程局部存储(threading.local)或 Logging handler 意外强引用 - 常见陷阱:
functools.lru_cache缓存未设maxsize、weakref.WeakKeyDictionary错写成普通dict、用类变量存实例引用
异步任务(asyncio)里忘了 await 或漏了 cancel() 就会吃内存
协程对象本身不占多少内存,但一旦被创建却没被调度或取消,它关联的帧对象(frame)、局部变量、上下文就会一直挂着——尤其在长周期服务中,这种“幽灵 task”越积越多。
排查和防御都得从事件循环入手:
- 用
asyncio.all_tasks(loop)列出所有存活 task,过滤掉done()为False且cancelled()为False的,它们就是可疑分子 - 启动 task 时务必用
asyncio.create_task()而非直接调用协程函数;取消时用task.cancel()+await task,别只 cancel 不 await - 日志打点要小心:
logging.info(f"start {obj}")中的obj若含大字段或闭包,可能让整个帧无法释放
第三方库的缓存策略常是隐藏泄漏源,尤其 ORM 和 http 客户端
django ORM 的 QuerySet、sqlAlchemy 的 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,但根因在缓存策略上。