33-内存泄漏排查工具箱-tracemalloc-objgraph与循环引用
2026/6/17 22:51:33 网站建设 项目流程

文章目录

  • 内存突然飙升?——Python 内存泄漏的排查工具箱:tracemalloc / objgraph / weakref
    • 导入语
    • 1 ~> 为什么 Python 还会有内存泄漏
      • 1.1 引用计数失效的场景
      • 1.2 全局容器无限膨胀
    • 2 ~> 工具一:`tracemalloc`——精确到行号的内存追踪
      • 2.1 基础用法
      • 2.2 在 Django 视图或定时任务中追踪
      • 2.3 真实排查——定时任务的内存膨胀
    • 3 ~> 工具二:`objgraph`——可视化对象引用链
      • 3.1 安装
      • 3.2 查看哪种类型的对象最多
      • 3.3 查看谁在引用这些对象
    • 4 ~> 工具三:`weakref`——打破循环引用的设计模式
      • 4.1 什么是弱引用
      • 4.2 在信号和回调中使用弱引用
      • 4.3 `WeakValueDictionary`——自动清理的缓存
    • 5 ~> Django 生产环境内存泄漏排查清单
    • 思考 && 总结
    • 结尾

内存突然飙升?——Python 内存泄漏的排查工具箱:tracemalloc / objgraph / weakref

最新推荐文章于 2026-07-19 12:00:00 发布 | 阅读 1.0k 阅读 | 分类:Python性能优化

📖文章简介:你的 Django 应用跑了一周后内存从 200MB 涨到 2GB——但 CPU 和数据库都正常。这就是内存泄漏的典型症状。Python 有垃圾回收,为什么还会有内存泄漏?本文从引用计数和 GC 失效的两个场景讲起——循环引用和全局缓存——然后逐一展开排查利器:tracemalloc模块精确追踪内存分配位置、objgraph直观显示哪些对象数量异常、weakref弱引用打破循环引用链。每一步都配有真实的 Django 调试过程——一个定时任务因为缓存字典无限膨胀导致 OOM、一个 ORM 对象因为信号处理中的引用未释放导致内存泄漏。排查完后 Django 应用内存从 2GB 降回 200MB。


🎬 个人主页:源码骑士

专栏传送门:《Android开发基础》《python基础课程》

⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂


🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"


导入语

2022 年 6 月,运维发了一张监控截图——一台跑 Django 的服务器内存使用从常驻 200MB 涨到了 1.8GB,呈线性增长趋势。CPU 稳定在 10%,数据库负载正常。没有报错,但这样下去一周内就会 OOM。

排查发现两个问题:一个定时任务里缓存了一个全局字典,字典不断膨胀从未清理——这是典型的内存泄漏。另一个是信号处理函数中持有了 ORM 对象的引用,对象被循环引用卡住,GC 无法回收。

Python 有垃圾回收,但引用计数和分代 GC 各有一块盲区。本文把工具箱里的三件工具全摊开——tracemallocobjgraphweakref——每件工具都配实际案例。


1 ~> 为什么 Python 还会有内存泄漏

1.1 引用计数失效的场景

classNode:def__init__(self,name):self.name=name self.ref=Nonea=Node("A")b=Node("B")a.ref=b b.ref=a# 循环引用deladelb# 两个对象引用计数都不是 0——GC 要到下一次扫描才回收

1.2 全局容器无限膨胀

# 最隐蔽的泄漏——全局变量或类属性被持续添加数据_request_cache={}# 全局字典defprocess_request(user_id,data):ifuser_idnotin_request_cache:_request_cache[user_id]=[]_request_cache[user_id].append(data)# 这个字典从不清空——每一次请求都在往里面塞数据

2 ~> 工具一:tracemalloc——精确到行号的内存追踪

2.1 基础用法

importtracemalloc tracemalloc.start()# 你的代码data=[1]*1000000# 分配 ~8MB# 取快照snapshot=tracemalloc.take_snapshot()# 按文件统计内存分配forstatinsnapshot.statistics("filename")[:5]:print(f"{stat.traceback}:{stat.size/1024/1024:.2f}MB")

2.2 在 Django 视图或定时任务中追踪

# 在 View 中嵌入 tracemallocimporttracemalloc tracemalloc.start()defreport_view(request):snapshot_before=tracemalloc.take_snapshot()# 业务逻辑data=generate_report()snapshot_after=tracemalloc.take_snapshot()diff=snapshot_after.compare_to(snapshot_before,"lineno")forstatindiff[:5]:print(f"+{stat.size_diff/1024}KB:{stat.traceback}")returnrender(request,"report.html",{"data":data})

输出示例:

+4821KB: D:\myproject\reports\utils.py:23 # data = [x for x in range(1000000)] +125KB: D:\myproject\reports\views.py:8 # context = {"data": data}

精确到代码行——这就是 tracemalloc 的价值。不是泛泛的"内存变大了",而是"reports/utils.py 第 23 行分配了 4.8MB"。

2.3 真实排查——定时任务的内存膨胀

那个 2022 年泄漏的定时任务,用tracemalloc定位后输出:

+48MB: D:\myproject\tasks\sync.py:31 # _cache[user_id].append(data) +12MB: D:\myproject\tasks\sync.py:28 # if user_id not in _cache: _cache[user_id] = []

一眼看到——全局字典_cache在不断增长且从未清除。修复方案:加了一条_cache.clear()在任务结束时调用,或者改用 LRU 缓存自动淘汰旧条目。


3 ~> 工具二:objgraph——可视化对象引用链

3.1 安装

pipinstallobjgraph

3.2 查看哪种类型的对象最多

importobjgraph# 查看最多的对象类型objgraph.show_most_common_types(limit=10)# 输出可能:# dict 18234# function 12341# list 8923# BorrowRecord 7641 ← 这个类型为什么这么多?

BorrowRecord有七千多个实例——而总共只有 200 个用户和 500 本书。这个数字异常——说明 ORM 对象没有被回收。

3.3 查看谁在引用这些对象

# 查看最新代中没有被回收的 BorrowRecord 对象roots=objgraph.get_leaking_objects()forobjinroots:ifisinstance(obj,BorrowRecord):print(obj,obj.id)

然后追踪引用链:

# 画出引用链图importrandom leaks=[objforobjinrootsifisinstance(obj,BorrowRecord)]ifleaks:objgraph.show_backrefs(leaks[:3],filename="leak.png",max_depth=5)

生成的图像会显示——这些 BorrowRecord 被某个全局字典_signal_cache引用着,而_signal_cache被一个函数闭包捕获——导致引用链无法断开。


4 ~> 工具三:weakref——打破循环引用的设计模式

4.1 什么是弱引用

importweakrefclassCache:def__init__(self):self.data={}cache=Cache()weak_ref=weakref.ref(cache)# 弱引用——不影响引用计数print(weak_ref())# 输出对象delcache# 强引用没了,对象被回收print(weak_ref())# 输出 None

weakref创建的引用不会增加引用计数——被引用对象可以在所有强引用都消失后被正常回收。

4.2 在信号和回调中使用弱引用

importweakrefclassSignalManager:"""管理回调——避免因为回调持有对象引用导致内存泄漏"""def__init__(self):self._receivers=[]defconnect(self,receiver):# 用弱引用保存回调——不影响接收者对象的生命周期self._receivers.append(weakref.ref(receiver))defsend(self,signal):alive=[]forrefinself._receivers:receiver=ref()ifreceiver:# 还活着则调用receiver(signal)alive.append(ref)# 保留这个引用self._receivers=alive# 清掉已失效的引用

4.3WeakValueDictionary——自动清理的缓存

fromweakrefimportWeakValueDictionaryclassModelCache:def__init__(self):self._cache=WeakValueDictionary()# key → 弱引用值defput(self,key,obj):self._cache[key]=objdefget(self,key):returnself._cache.get(key)cache=ModelCache()# 当外部对 obj 的引用全部消后——cache 中的条目自动消失

5 ~> Django 生产环境内存泄漏排查清单

  1. 全局字典/列表——检查所有_cache = {}_data = []这种全局容器,确保它们有限增长
  2. 信号回调——post_savepost_delete等信号处理函数中不要持有对发送者(sender)的强引用
  3. 类属性——MyClass.big_list = []在所有实例间共享——不断添加会膨胀
  4. 闭包——定时任务中的闭包如果捕获了外部的大列表,在任务重复执行时会造成累积
  5. Django QuerySet——被.all()或其他惰性查询创建后,如果不被释放,所有缓存的对象都留在内存

排查命令:

python-mtracemalloc--tracebackmyproject/manage.py runserver

思考 && 总结

内存泄漏三件工具的适用场景:

工具什么时候用输出什么
tracemalloc精确追踪"哪行代码分配了多少内存"分配大小 + 文件 + 行号
objgraph排查"哪种类型的对象数量异常"类型计数 + 引用链可视化
weakref设计上预防"循环引用和信号泄漏"弱引用不增加引用计数

结尾

内存排查工具箱讲完了。感谢阅读!

源码骑士 — 源码级拆解,从底层看透技术

👀关注:跟博主一起从源码视角深耕底层原理

❤️点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬评论:分享你的经验或疑问,一起交流

🔄一键四连:别忘了给博主一键四连!

🗡️寄语:tracemalloc 告诉你哪个函数偷了你的内存,objgraph 告诉你谁还在引用它。

结语:Python 内存泄漏不是靠猜能解决的——tracemalloc + objgraph + weakref 三件套,定位→分析→修复一条龙。下篇进入并发模型对比——多线程 vs 多进程 vs 协程。一键四连!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询