tracemalloc.start() 必须在脚本最顶部、所有导入和初始化前调用,参数建议设为25以获取完整调用栈;取快照时需用filter过滤仅保留项目源码路径;应使用compare_to比较增量而非total,并关注size_diff和count_diff均为正的条目。

tracemalloc.start() 必须在程序最早期调用
如果在内存已大量分配后再启动 tracemalloc.start(),它只会记录后续的分配,无法回溯到初始化阶段的泄漏源头。常见错误是把它放在 main 函数里、或某个模块导入后才调用——此时很多全局对象、缓存、单例可能早已创建。
正确做法是:在脚本最顶部(if __name__ == "__main__": 之前)、甚至比日志配置还早的位置启用:
import tracemalloc tracemalloc.start(25) # 25 表示保存最多 25 层调用栈 <p>import logging</p><h1>... 其他 import</h1>
tracemalloc.start() 的参数建议设为 25 或更高(默认是 1),否则堆栈过短,你只能看到 list.append 这类底层调用,看不到具体哪行业务代码触发的。
对比 snapshot 时要用 filter_traces 过滤无关路径
直接调用 tracemalloc.take_snapshot() 会捕获所有分配,包括标准库、第三方包(如 requests、numpy)内部的临时对象,干扰判断。不加过滤的话,top 10 占用里常出现 /lib/python3.x/.../abc.py 这类路径,毫无定位价值。
应使用 filter_traces 只保留项目源码路径:
snap1 = tracemalloc.take_snapshot()
# 模拟一段时间运行
do_some_work()
snap2 = tracemalloc.take_snapshot()
<h1>只保留当前项目目录下的文件(假设你的代码在 ./src)</h1><p>current_dir = os.path.abspath("./src")
filter = tracemalloc.Filter(True, current_dir)
snap2 = snap2.filter_traces([filter])</p>
关键点:
-
Filter(True, ...)表示“包含”,False是排除 - 路径必须是绝对路径,相对路径会被忽略
- 多个模块可传入多个
Filter,比如同时保留./src和./tests
用 compare_to 找增长最多的 allocation,不是 total
很多人看 stat.total,但内存泄漏的本质是「持续新增未释放的对象」,所以真正该盯的是两次快照间「新增分配量」最大的位置。用 snapshot2.compare_to(snapshot1, 'lineno') 才能暴露问题代码行。
示例输出中重点关注 size_diff(字节数变化)和 count_diff(对象数变化)都为正的条目:
for stat in top_stats:
print(stat.size_diff, stat.count_diff, stat.traceback.format())
典型泄漏信号:
-
size_diff > 100KB且count_diff > 10(说明反复新建中等大小对象) -
size_diff稳定增长(每轮循环+8KB)→ 很可能是 list.append 或 dict[key] = value 在不断累积 - 同一行代码出现在多个 top 条目中 → 高概率是泄漏点
注意异步/多线程场景下 snapshot 的时机偏差
tracemalloc 默认只跟踪主线程。如果你用 asyncio、threading 或 multiprocessing,主线程的 snapshot 会漏掉子线程/协程里的分配。
解决方式分情况:
- 多线程:在每个工作线程内单独调用
tracemalloc.start(),并在其结束前take_snapshot();主线程无法汇总 - asyncio:确保事件循环启动前已在主线程启用
tracemalloc,因为大部分协程仍在主线程执行;但loop.run_in_executor中的阻塞调用仍需额外处理 - fork 进程:子进程不继承 tracemalloc 状态,必须在
os.fork()后手动start()
最稳妥的做法是:先确认泄漏是否复现在单线程同步模式下;若只在并发时出现,再针对性地在 worker 初始化逻辑里插入 tracing。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











