人人都会AI编程

30.6 循环引用与内存泄漏

更新时间:2026-07-12

Python 有自动内存管理,大多数时候不用管对象什么时候释放。但循环引用是一个经典陷阱:两个或多个对象互相持有对方的引用,形成一个“环”,导致引用计数永远不为零,如果垃圾回收器不能及时回收,就会造成内存泄漏。

循环引用是怎么发生的?

很简单,一个对象持有另一个对象,另一个对象又反向持有了它。常见场景:

  • 双向关联的类,比如父子节点互相引用。
  • 列表或字典等容器内部相互嵌套,自己里面包含了自己。
  • 自定义异常对象持有 traceback,而 traceback 又间接引用了异常对象本身。

典型例子:

class Node:
    def __init__(self, name):
        self.name = name
        self.parent = None
        self.children = []

root = Node("root")
child = Node("child")
root.children.append(child)
child.parent = root

# root 和 child 相互引用
del root, child  # 此时引用计数不为零,如果没有 gc 介入,这两个对象不会立即释放

为什么循环引用会导致内存泄漏?

Python 主要基于引用计数来回收内存:一个对象引用计数归零,立刻释放。
循环引用中的对象,你中有我我中有你,各自的引用计数至少为 1,无法归零。
CPython 有一个分代垃圾回收器专门处理循环引用,它会定期扫描并尝试打破这种环,但扫描有开销,且如果代码中持有全局或长生命周期对象的引用,可能让循环引用中的对象一直被标记为存活,最终内存不断增长。

如何发现循环引用?

  • 直觉:程序运行时内存只增不减,持续跑一段时间后 OOM(内存溢出)。
  • 工具
  • objgraph 库:可视化对象的引用关系,直接找到哪些对象形成了环。
  • tracemalloc:标准库内存追踪,对比快照,看哪些对象的分配量异常增长。
  • gc 模块:启用 gc.DEBUG_SAVEALL 可以将所有作为垃圾回收但因循环引用无法释放的对象保存到 gc.garbage 中,直接检查这些对象。

解决方案:怎么避免和打破循环引用?

1. 使用弱引用 weakref
如果某个方向的引用并不决定对象的生命周期,就换成弱引用。弱引用不会增加引用计数,也就不会阻止垃圾回收。

import weakref

class Node:
    def __init__(self, name):
        self.name = name
        self.parent = None
        self.children = []

root = Node("root")
child = Node("child")
root.children.append(child)
child.parent = weakref.ref(root)  # 弱引用,不会造成循环

常用 weakref.ref 创建弱引用,或者用 weakref.WeakValueDictionaryweakref.WeakSet 等容器自动管理。

2. 显式打破循环
在明确知道对象不再使用时,手动将相关属性设为 None,切断引用链。

def cleanup(node):
    node.parent = None
    node.children.clear()

常见于自定义类的 close()cleanup() 方法,或在 del 中操作(但要小心 del 本身可能因循环引用而永远不被调用)。

3. 使用上下文管理器或 with 语句
利用 enter/exit 确保资源在退出时自动释放,例如文件、网络连接等,间接避免长生命周期的持有。

4. 重新设计数据结构
尽量避免“双向引用是强引用”的设计。例如,树结构可以用父节点到子节点的强引用,而从子节点查找父节点可以用 weakref,或者在遍历时通过算法传递父节点而不存储引用。

需要注意的坑

  • 不要依赖 del:因为循环引用中的对象 del 方法可能永远不会被调用(Python 的垃圾回收器无法确定安全的销毁顺序),会导致资源泄露。
  • gc.collect() 不能解决所有问题:如果对象被全局变量或其他长生命周期容器直接或间接引用,即使存在循环引用,垃圾回收也不会回收它们,因为它们被认为是可达的。
  • 性能敏感场景小心弱引用:弱引用访问比直接引用稍慢,且使用不当可能导致弱引用失效后取回 None,需要额外判断。

小结

  • 循环引用是 Python 内存泄漏的常见根源,尤其在复杂的数据结构、缓存、事件回调中容易出现。
  • 预防为主:设计阶段就评估是否必须使用强双向引用,优先考虑弱引用。
  • 发现要快:结合 tracemallocobjgraph 等工具,在测试阶段就主动检查内存趋势。
  • 修复要彻底:明确对象生命周期,在合适时机打破循环。