无循环引用的Python对象为何仍被垃圾回收?场景对比问询
关于Python引用计数与垃圾回收的差异问题
现象观察
当函数循环中创建的大量对象无外部引用时,会通过Python引用计数立即回收;但如果将这些对象存入列表,函数退出时,即便对象没有循环引用,也会由垃圾回收机制处理,而非引用计数。
我们通过调用run函数的两个场景(with_append=True/False)做对比:场景1触发垃圾回收,程序耗时翻倍;场景2无垃圾回收活动(可通过py-spy native模式观测)。
示例代码
import time COUNT = 10000000 class User: def __init__(self, name): self.name = name def run(with_append): l = [] for i in range(COUNT): u = User(f"user {i}") if with_append: l.append(u) ts = time.time() run(with_append=True) print("function completed - ", time.time() - ts)
场景对比
- 场景1(with_append=True):程序输出耗时10.940133094787598秒,
py-spy显示53%时间用于gc_collect_main。 - 场景2(with_append=False):输出耗时4.351471424102783秒,无垃圾回收相关耗时。
疑问解答
为什么两类场景未均使用引用计数释放内存?
在with_append=False的场景中,每个User对象在循环迭代结束后,引用计数直接降为0:变量u每次循环都会被新对象覆盖,旧对象没有其他引用,因此触发引用计数的即时回收,无需垃圾回收介入。
而with_append=True的场景中,所有User对象都被列表l持有引用,它们的引用计数至少为1。当函数run执行完毕,局部变量l的引用计数归0,此时所有User对象的引用计数才集体降为0。Python在处理大量对象同时被释放的情况时,会触发垃圾回收的辅助流程——虽然这些对象没有循环引用,但批量释放数百万对象会触发垃圾回收的扫描与整理逻辑,因此由垃圾回收机制完成最终清理。
垃圾回收耗时更长的原因是什么?
引用计数回收是即时且轻量的:对象引用计数归0时,直接调用析构函数释放内存,过程简单直接,开销极小。
而垃圾回收的gc_collect_main需要完成一系列复杂操作:
- 扫描所有存活对象,标记可回收对象(即便无循环引用,标记过程仍需执行)
- 整理内存分配器的缓存,处理内存碎片
- 更新垃圾回收的触发阈值等统计信息
这些批量处理的步骤相比单个对象的即时回收,开销要大得多,尤其当处理数百万对象时,耗时自然显著增加。
内容的提问来源于stack exchange,提问作者Holzner
相关产品推荐
相关产品推荐

