news 2026/10/16 6:35:25

Python内存泄漏真相:循环引用才是元凶

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python内存泄漏真相:循环引用才是元凶

一、先说结论:内存泄漏不一定是代码写错了

最近在维护一个 Python 服务时,发现进程内存占用一路飙升,从最初的 300MB 涨到 2GB 多,重启之后过两天又涨回去。一开始我怀疑是某个第三方库没释放资源,排查了半天才发现,问题出在代码里的循环引用上。这篇文章记录一下我当时的排查思路和定位过程,希望能给同样踩坑的朋友一点参考。

二、现象:内存只涨不降

服务跑起来之后,我用ps aux观察进程的 RSS 内存,发现它呈现一种"阶梯式上涨"的规律:每次处理完一批任务,内存就涨一截,但任务结束后并不会回落。这基本可以排除临时大对象的问题,更像是对象被"留住"了,垃圾回收器一直收不掉。

为了确认是不是 Python 的垃圾回收机制出了问题,我先在代码里手动触发了一次gc.collect(),结果内存纹丝不动。这说明问题不在引用计数,而在循环引用导致的"不可达但无法回收"的对象。

三、定位工具:gc 模块和 objgraph

Python 自带的gc模块是排查这类问题最趁手的工具。我写了一段简单的脚本,把当前所有未被回收的对象按类型统计出来:

import gc import collections gc.collect() counts = collections.Counter(type(obj).__name__ for obj in gc.get_objects()) for name, cnt in counts.most_common(20): print(f"{name}: {cnt}")

跑完之后,我发现TaskHandler和Callback这两个类的实例数量异常多,而且数量还在持续增长。直觉告诉我,问题就藏在这两个类的关系里。

四、找到元凶:互相持有引用

我翻了一下业务代码,发现TaskHandler里保存了一个Callback的引用,而Callback又反过来持有TaskHandler的引用。两个对象互相"抱死",引用计数永远不为零,垃圾回收器虽然能检测到循环引用,但如果对象还定义了__del__方法,就会进入"不可回收"的名单。

我用objgraph画了一下引用关系图,确认了这条引用链:

import objgraph objgraph.show_refs( [handler], filename="refs.png", max_depth=3 )

图里清清楚楚地显示,TaskHandler和Callback之间有一条双向箭头,这就是内存泄漏的根源。

五、修复方案:打破循环引用

定位到问题之后,修复其实很简单。我选择了两种方式:

  • 把Callback里对TaskHandler的强引用改成弱引用,用weakref.ref()或者weakref.proxy()来持有。
  • 在对象不再需要时,显式把引用置为None,主动断开引用链。

改完之后,我重新跑了一遍压力测试,内存曲线终于变得平稳,任务处理完内存也会正常回落。下面是修复后的核心代码片段:

import weakref class TaskHandler: def __init__(self): self.callback = None def set_callback(self, callback): # 用弱引用持有回调,避免循环引用 self.callback = weakref.ref(callback) class Callback: def __init__(self, handler): # 这里不再强引用 handler,改用弱引用 self.handler = weakref.proxy(handler)

六、几点排查建议

这次踩坑让我总结出几条经验,分享给大家:

  • 内存泄漏不一定是"没释放资源",循环引用同样会让对象永远无法回收。
  • 排查时优先用gc.get_objects()统计对象数量,比盲目加日志高效得多。
  • 如果对象定义了__del__,循环引用会导致它彻底无法回收,尽量避免在业务类里定义__del__。
  • 写回调、事件监听、观察者模式这类代码时,多想想引用方向,能用弱引用就别用强引用。

内存问题排查起来往往比较隐蔽,但只要掌握了正确的工具和思路,定位起来并不难。希望这篇文章能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 6:34:47

采购部降本增效12个方法

采购最怕什么? 不是供应商报价高,而是价格谈下来了,质量却出了问题;不是订单下得少,而是仓库堆满了货,生产急用的物料却迟迟不到;不是采购每天不够忙,而是询价、催货、对账、处理异常…

作者头像 李华
网站建设 2026/10/16 6:33:48

page_alloc numa_zonelist_order_handler

sysctl 处理函数和节点负载数组。一、numa_zonelist_order[] 与长度宏static char numa_zonelist_order[] "Node"; #define NUMA_ZONELIST_ORDER_LEN 161. numa_zonelist_order[]当前 zonelist 排序策略的字符串表示,初始为 "Node"。这是 sysc…

作者头像 李华
网站建设 2026/10/16 6:32:57

易语言系统处理运行()详解:解决界面假死与消息泵机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华