python内存泄漏主因是对象被隐性持有而非未调gc.collect(),可用objgraph.show_growth()定位异常增长类型,再用gc.get_referrers()逆向追踪强引用源,并结合rss监控与gc调试确认真实泄漏。

Python 脚本内存持续上涨,大概率不是“没调 gc.collect()”,而是对象根本没被识别为可回收——比如被全局容器悄悄持有、含 __del__ 的循环引用未被扫描,或第三方库内部缓存长期驻留。
用 objgraph.show_growth() 快速揪出暴涨的类型
这是最轻量的第一步,不依赖快照、不改逻辑,几行代码就能暴露异常堆积:
- 在循环入口/关键函数前后各调一次
objgraph.show_growth(limit=10),重点关注数字跳变大的类型(如dict、list、自定义类实例) - 若某类数量从 120 → 1500 → 2800 持续增长,基本可锁定泄漏源头就在该路径内
- 注意:
show_growth只统计“新增未回收”对象,不反映单个对象大小;它帮你缩小范围,不是最终答案 - 常见误判:看到
weakref或frame数量多别慌——它们常是临时产物,真正危险的是MyDataModel这类业务类持续增涨
用 gc.get_referrers(obj) 追查谁在强引用这个对象
当你已定位到一个可疑对象(比如某个反复创建却没消失的 ConfigParser 实例),下一步就是查清“谁还抓着它不放”:
- 先用
sys.getrefcount(obj)确认引用数是否异常高(注意该函数自身加 1) - 再调
gc.get_referrers(obj),返回持有该对象引用的所有容器或变量名 - 典型结果:
[<module>, [<configparser instance>], {'cfg': <configparser instance>}]</configparser></configparser></module>——说明它被模块级列表和字典同时持有 - 若返回里出现
<frame object>,说明某个函数栈帧还在引用它(比如闭包、lambda、未退出的生成器) - 慎用:对大型对象(如 DataFrame)直接查 referrers 可能卡死,先用
tracemalloc定位分配点更稳妥
为什么 gc.collect() 调了也没用?检查这三件事
gc.collect() 失效,往往因为对象压根不在 GC 的扫描范围内:
- 含
__del__方法的对象:GC 默认跳过这类循环引用检测,避免析构顺序问题。改用weakref.finalize替代 - 被模块级变量持有:比如
cache = []在文件顶部,每次cache.append(obj)都让 obj 永远存活 - 第三方库内部缓存:如
requests.Session()的连接池、sqlalchemy.engine的 statement 缓存,它们不走 Python GC,得查对应库的清理 API(如session.close()) - 验证方法:运行
import gc; gc.set_debug(gc.DEBUG_STATS); gc.collect(),观察输出中是否有 “collected 0 objects from …” —— 若第 0 代回收数长期为 0,说明新对象根本没进 GC 流水线
别只盯对象数量,RSS 才是真实水位线
内存是否真高,最终看操作系统分配给进程的物理内存(RSS),而不是 Python 对象计数:
- 用
psutil.Process().memory_info().rss每秒采样,画趋势图:平稳上升 = 泄漏;阶梯式跳变 = 大对象创建(如读取新 chunk) - 对比
rss和vms:若 RSS 涨而 VMS 不变,问题在 Python 层引用未释放;若两者同步暴涨,可能是 NumPy/Cython 底层分配未回收 - 特别注意:
objgraph和tracemalloc都无法捕获 mmap 分配或 C 扩展私有堆内存,此时需结合gdb或valgrind(仅限 Linux)
真正难处理的从来不是“怎么查”,而是“谁在隐性持有”——比如 logging handler 缓存 record、信号回调注册后忘记注销、或者一个被弱引用容器间接持有的大字典。这些链路往往跨模块、跨库,必须从 RSS 趋势出发,用 get_referrers 一层层逆向剥开,而不是依赖某个工具一键定论。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











