33-内存泄漏排查工具箱-tracemalloc-objgraph与循环引用

文章目录

  • [内存突然飙升?------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 各有一块盲区。本文把工具箱里的三件工具全摊开------tracemallocobjgraphweakref------每件工具都配实际案例。


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 生产环境内存泄漏排查清单

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

排查命令:

bash 复制代码
python -m tracemalloc --traceback myproject/manage.py runserver

思考 && 总结

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

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

结尾

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

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

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

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

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

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

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

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

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