文章目录
- [内存突然飙升?------Python 内存泄漏的排查工具箱:tracemalloc / objgraph / weakref](#内存突然飙升?——Python 内存泄漏的排查工具箱:tracemalloc / objgraph / weakref)
-
- 导入语
- [1 ~> 为什么 Python 还会有内存泄漏](#1 ~> 为什么 Python 还会有内存泄漏)
-
- [1.1 引用计数失效的场景](#1.1 引用计数失效的场景)
- [1.2 全局容器无限膨胀](#1.2 全局容器无限膨胀)
- [2 ~> 工具一:`tracemalloc`------精确到行号的内存追踪](#2 ~> 工具一:
tracemalloc——精确到行号的内存追踪) -
- [2.1 基础用法](#2.1 基础用法)
- [2.2 在 Django 视图或定时任务中追踪](#2.2 在 Django 视图或定时任务中追踪)
- [2.3 真实排查------定时任务的内存膨胀](#2.3 真实排查——定时任务的内存膨胀)
- [3 ~> 工具二:`objgraph`------可视化对象引用链](#3 ~> 工具二:
objgraph——可视化对象引用链) -
- [3.1 安装](#3.1 安装)
- [3.2 查看哪种类型的对象最多](#3.2 查看哪种类型的对象最多)
- [3.3 查看谁在引用这些对象](#3.3 查看谁在引用这些对象)
- [4 ~> 工具三:`weakref`------打破循环引用的设计模式](#4 ~> 工具三:
weakref——打破循环引用的设计模式) -
- [4.1 什么是弱引用](#4.1 什么是弱引用)
- [4.2 在信号和回调中使用弱引用](#4.2 在信号和回调中使用弱引用)
- [4.3 `WeakValueDictionary`------自动清理的缓存](#4.3
WeakValueDictionary——自动清理的缓存)
- [5 ~> Django 生产环境内存泄漏排查清单](#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 各有一块盲区。本文把工具箱里的三件工具全摊开------tracemalloc、objgraph、weakref------每件工具都配实际案例。
1 ~> 为什么 Python 还会有内存泄漏
1.1 引用计数失效的场景
python
class Node:
def __init__(self, name):
self.name = name
self.ref = None
a = Node("A")
b = Node("B")
a.ref = b
b.ref = a # 循环引用
del a
del b # 两个对象引用计数都不是 0------GC 要到下一次扫描才回收
1.2 全局容器无限膨胀
python
# 最隐蔽的泄漏------全局变量或类属性被持续添加数据
_request_cache = {} # 全局字典
def process_request(user_id, data):
if user_id not in _request_cache:
_request_cache[user_id] = []
_request_cache[user_id].append(data)
# 这个字典从不清空------每一次请求都在往里面塞数据
2 ~> 工具一:tracemalloc------精确到行号的内存追踪
2.1 基础用法
python
import tracemalloc
tracemalloc.start()
# 你的代码
data = [1] * 1000000 # 分配 ~8MB
# 取快照
snapshot = tracemalloc.take_snapshot()
# 按文件统计内存分配
for stat in snapshot.statistics("filename")[:5]:
print(f"{stat.traceback}: {stat.size / 1024 / 1024:.2f} MB")
2.2 在 Django 视图或定时任务中追踪
python
# 在 View 中嵌入 tracemalloc
import tracemalloc
tracemalloc.start()
def report_view(request):
snapshot_before = tracemalloc.take_snapshot()
# 业务逻辑
data = generate_report()
snapshot_after = tracemalloc.take_snapshot()
diff = snapshot_after.compare_to(snapshot_before, "lineno")
for stat in diff[:5]:
print(f"+{stat.size_diff / 1024}KB: {stat.traceback}")
return render(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 安装
bash
pip install objgraph
3.2 查看哪种类型的对象最多
python
import objgraph
# 查看最多的对象类型
objgraph.show_most_common_types(limit=10)
# 输出可能:
# dict 18234
# function 12341
# list 8923
# BorrowRecord 7641 ← 这个类型为什么这么多?
BorrowRecord 有七千多个实例------而总共只有 200 个用户和 500 本书。这个数字异常------说明 ORM 对象没有被回收。
3.3 查看谁在引用这些对象
python
# 查看最新代中没有被回收的 BorrowRecord 对象
roots = objgraph.get_leaking_objects()
for obj in roots:
if isinstance(obj, BorrowRecord):
print(obj, obj.id)
然后追踪引用链:
python
# 画出引用链图
import random
leaks = [obj for obj in roots if isinstance(obj, BorrowRecord)]
if leaks:
objgraph.show_backrefs(leaks[:3], filename="leak.png", max_depth=5)
生成的图像会显示------这些 BorrowRecord 被某个全局字典 _signal_cache 引用着,而 _signal_cache 被一个函数闭包捕获------导致引用链无法断开。
4 ~> 工具三:weakref------打破循环引用的设计模式
4.1 什么是弱引用
python
import weakref
class Cache:
def __init__(self):
self.data = {}
cache = Cache()
weak_ref = weakref.ref(cache) # 弱引用------不影响引用计数
print(weak_ref()) # 输出对象
del cache # 强引用没了,对象被回收
print(weak_ref()) # 输出 None
weakref 创建的引用不会增加引用计数------被引用对象可以在所有强引用都消失后被正常回收。
4.2 在信号和回调中使用弱引用
python
import weakref
class SignalManager:
"""管理回调------避免因为回调持有对象引用导致内存泄漏"""
def __init__(self):
self._receivers = []
def connect(self, receiver):
# 用弱引用保存回调------不影响接收者对象的生命周期
self._receivers.append(weakref.ref(receiver))
def send(self, signal):
alive = []
for ref in self._receivers:
receiver = ref()
if receiver: # 还活着则调用
receiver(signal)
alive.append(ref) # 保留这个引用
self._receivers = alive # 清掉已失效的引用
4.3 WeakValueDictionary------自动清理的缓存
python
from weakref import WeakValueDictionary
class ModelCache:
def __init__(self):
self._cache = WeakValueDictionary() # key → 弱引用值
def put(self, key, obj):
self._cache[key] = obj
def get(self, key):
return self._cache.get(key)
cache = ModelCache()
# 当外部对 obj 的引用全部消后------cache 中的条目自动消失
5 ~> Django 生产环境内存泄漏排查清单
- 全局字典/列表 ------检查所有
_cache = {}、_data = []这种全局容器,确保它们有限增长 - 信号回调 ------
post_save、post_delete等信号处理函数中不要持有对发送者(sender)的强引用 - 类属性 ------
MyClass.big_list = []在所有实例间共享------不断添加会膨胀 - 闭包------定时任务中的闭包如果捕获了外部的大列表,在任务重复执行时会造成累积
- Django QuerySet ------被
.all()或其他惰性查询创建后,如果不被释放,所有缓存的对象都留在内存
排查命令:
bash
python -m tracemalloc --traceback myproject/manage.py runserver
思考 && 总结
内存泄漏三件工具的适用场景:
| 工具 | 什么时候用 | 输出什么 |
|---|---|---|
tracemalloc |
精确追踪"哪行代码分配了多少内存" | 分配大小 + 文件 + 行号 |
objgraph |
排查"哪种类型的对象数量异常" | 类型计数 + 引用链可视化 |
weakref |
设计上预防"循环引用和信号泄漏" | 弱引用不增加引用计数 |
结尾
内存排查工具箱讲完了。感谢阅读!
源码骑士 --- 源码级拆解,从底层看透技术
👀 关注:跟博主一起从源码视角深耕底层原理
❤️ 点赞:让优质内容被更多人看见
⭐ 收藏:核心知识点存好,随用随查
💬 评论:分享你的经验或疑问,一起交流
🔄 一键四连:别忘了给博主一键四连!
🗡️ 寄语:tracemalloc 告诉你哪个函数偷了你的内存,objgraph 告诉你谁还在引用它。
结语:Python 内存泄漏不是靠猜能解决的------tracemalloc + objgraph + weakref 三件套,定位→分析→修复一条龙。下篇进入并发模型对比------多线程 vs 多进程 vs 协程。一键四连!