cpython的循环引用可被gc自动回收,但含__del__或特定c扩展类型的对象会进入gc.garbage导致内存泄漏;应优先用gc.get_referrers()定位外部强引用,并检查gc.garbage、启用调试模式或使用objgraph辅助分析,同时通过弱引用、显式断开和避免__del__预防泄漏。

用 gc.get_referrers() 定位谁在持有对象
循环引用本身不会阻止 gc 回收(CPython 的循环垃圾收集器能处理),但一旦对象定义了 __del__ 方法,或属于某些 C 扩展类型(如某些 NumPy 数组、threading.Lock),GC 就会把它们放进 gc.garbage 而不再尝试清理。这时候对象实际“活”着,却没人能访问到——典型的内存泄露表象。
关键不是找“谁引用了谁”,而是找“谁还在外部强引用着本该被回收的对象”。gc.get_referrers(obj) 能列出所有直接引用 obj 的对象,是定位源头最直接的手段:
import gc
<p>class Node:
def <strong>init</strong>(self, name):
self.name = name
self.parent = None
self.children = []</p><h1>构造典型循环引用</h1><p>a = Node("a")
b = Node("b")
a.children.append(b)
b.parent = a</p><h1>强制触发 GC 并检查</h1><p>gc.collect()
print(len(gc.get_referrers(a))) # 可能 >1 —— 比如还有全局变量、日志缓存、调试器引用等</p>
- 调用前先
gc.collect(),确保待查对象已进入不可达状态 - 如果
gc.get_referrers(obj)返回非空列表,逐个检查返回项:type(x)、repr(x),尤其注意模块级字典、缓存dict、weakref.WeakKeyDictionary的误用(它不持强引用,但如果你用了普通dict当缓存就危险) - 避免在生产环境频繁调用——它会暂停解释器并遍历整个堆,开销大
检查 gc.garbage 里有没有“卡住”的对象
当 GC 发现无法安全清理的循环(比如含 __del__),会把整个环塞进 gc.garbage 列表,且不再自动处理。这是泄露最明确的信号。
启用垃圾回收调试并观察:
import gc
<p>gc.set_debug(gc.DEBUG_UNCOLLECTABLE) # 输出无法回收的对象信息</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill4102" title="Shadows Python Sensei"><img
src="https://img.php.cn/upload/skill/000/000/081/178990406882325.jpg" alt="Shadows Python Sensei" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill4102" title="Shadows Python Sensei" class="overflowclass">Shadows Python Sensei</a>
<p class="overflowclass">Python 最佳实践助手——代码规范、设计模式、性能优化、测试与类型注解。适用于编写或审查 Python 代码。</p>
</div>
<a rel="nofollow" href="/xiazai/skill4102" title="Shadows Python Sensei" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><h1>或更轻量:运行后检查</h1><p>gc.collect()
print("Uncollectable:", len(gc.garbage))
for obj in gc.garbage[:3]: # 只看前几个,避免爆炸
print(type(obj), id(obj))</p>
-
gc.garbage是可变列表,GC 不会自动清空它;必须手动清理(del gc.garbage[:])或修复引用逻辑,否则每次gc.collect()都会往里追加 - 常见“卡住”类型:
threading._RLock、自定义__del__类、某些 ORM 实例(如 SQLAlchemy 未关闭 session)、用ctypes创建的结构体 - 如果
gc.garbage持续增长,说明有稳定路径在不断产生带终结器的循环引用
用 objgraph 可视化引用链(需安装)
objgraph 不是标准库,但它能快速生成引用图,对复杂嵌套结构特别有用——比手写 gc.get_referrers 嵌套调用直观得多。
# pip install objgraph import objgraph <h1>查找某类实例数量是否异常增长</h1><p>objgraph.show_growth(limit=5)</p><h1>对某个可疑对象画出两层引用者</h1><p>objgraph.show_backrefs([my_obj], max_depth=2, too_many=10)</p>
-
show_growth()每次调用对比上一次快照,输出新增最多的类型——适合长期运行服务中定时采样 -
show_backrefs()输出的是 dot 图,需配合graphviz渲染;若只想看文本路径,用objgraph.find_backref_chain(my_obj, objgraph.is_proper_module) - 注意过滤掉
frame和traceback对象——它们是临时的,容易干扰判断
避免循环引用的设计习惯
排查是补救,预防才是关键。很多循环引用其实可以靠约定或弱引用来切断。
- 父子关系中,子对象用
weakref.ref(parent)存父引用,而不是直接存parent实例 - 缓存场景统一用
weakref.WeakValueDictionary或functools.lru_cache(后者自带弱引用语义) - 显式断开:在对象生命周期结束时(如
close()方法里)手动设self.parent = None、self._cache.clear() - 避免在类定义里写
__del__;真需要资源清理,用上下文管理器(__enter__/__exit__)或显式close()
真正难搞的往往不是纯 Python 循环,而是 Python 对象和 C 扩展之间隐式的双向持有——比如一个 Pandas DataFrame 持有底层 NumPy 数组,而该数组又被某个 Cython 函数通过指针反向引用。这种时候,gc.get_referrers 可能只显示 <capsule></capsule>,就得结合扩展模块文档和源码来确认释放契约。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










