引用与垃圾回收
引用与垃圾回收
前面的可变对象、闭包和生成器,都涉及同一个问题:是谁还在持有对象。这里以 CPython 3.12 常规 GIL 构建为基线,把名字、强引用、对象销毁与循环回收串起来。
对象如何保持存活
Python 与 CPython
Python 是语言及其规则,CPython 是主要用 C 实现的解释器;PyPy 等是其他实现。引用计数、pymalloc、具体字节码是 CPython 细节,不能仅凭「Python 语法相同」推断内存回收时机相同。
赋值建立引用
赋值把名字绑定到对象,不会自动复制对象。a = []; b = a 中两个名字指向同一个列表;b.append(1) 可通过 a 观察到,而 b = [] 只是换了 b 的绑定。
身份与生命周期
is 判断是不是同一对象,== 比较值,id 在对象生命周期内标识它。CPython 的 id 通常对应地址,但旧对象销毁后地址可复用;id 不能当跨生命周期的业务唯一编号。
del 删除绑定
del a 删除名字绑定,del xs[0] 删除容器中的引用,不是强制释放该对象。其他强引用还在,对象就仍可存活;即使对象已销毁,底层内存也可能先留给分配器复用。
返回值、闭包与暂停状态
不能这样推断。局部名字关联的对象可能被返回值、容器或闭包继续引用;生成器/协程的执行状态还能跨暂停保留。Python 调用帧与原生线程栈也不是一一等同的概念。
引用计数与循环 GC
引用计数
记录强引用的增减,普通对象计数归零时通常开始销毁,这是常规 CPython 中很多对象能及时释放的原因。但不朽对象、终结与对象复活等存在例外,计数也不应被业务代码拿来决定对象所有权。
循环引用的局限
a 指向 b、b 指向 a,即使外部引用全没了,内部引用仍使计数不为零。单纯数引用无法判断这一组对象是否已不可达,因此还需循环垃圾收集器识别整组无外部支持的对象。
两种回收机制配合
引用计数处理大量普通生命周期,循环 GC 补上不可达引用环。GC 不会清理业务仍强引用的缓存;列表里不断积累对象,即使每次 gc.collect 也不能把它们当垃圾删掉。
被 GC 跟踪的对象
不会。主要跟踪可能构成引用环的对象,例如许多容器和用户实例;整数等一般无需跟踪,部分容器还可优化为不跟踪。gc.is_tracked 可观察,不能把 gc.get_objects 当作全部内存对象清单。
CPython 3.12 的分代
利用许多对象很快死亡、少数对象存活较久的规律,区别扫描频率。CPython 3.12 使用 0、1、2 三代;这是循环 GC 的分组,不表示 Python 有三块独立的 Java 式堆空间。
判断不可达的引用环
概念上对候选对象的引用计数做临时分析,扣除候选内部引用,找出仍有外部支持的对象,再保留从它们可达的部分;剩余不可达部分可回收。不是看到互相引用就删,也不是只要计数大于零就保留。
自动 GC 的触发
自动收集与分配/释放计数和代际阈值有关,不是每隔固定几秒扫描,也不是「第 700 个对象必定立刻 full GC」。用 gc.get_count/get_threshold/get_stats 观察,触发条件与实际运行收集的安全时点要区分。
关闭自动 GC 与手动回收
gc.disable 关闭自动循环收集,不关闭引用计数;手动 gc.collect 仍可执行。用前确认是否产生环,恢复原状态;不要在 collector 正在执行时递归调用 collect,也别为追求一次延迟数据长期漏掉循环垃圾。
gc.garbage 与调试标志
不一定。DEBUG_SAVEALL 会把原本可清理的不可达对象也保存在 gc.garbage,DEBUG_LEAK 包含这一行为。先核对 debug flags,排查结束恢复设置并释放诊断引用,避免诊断工具自己留住垃圾。
终结器与循环引用
现代 CPython 对普通 Python 终结器已有安全终结支持,不能继续背「有 del 的环一定泄漏」。C 扩展终结器、复活和复杂资源依赖另有边界;PEP 442 从 Python 3.4 引入相关改变。
自动循环 GC 是引用计数的补充;gc.disable() 的范围见gc 文档,不能理解成关闭所有对象释放。
弱引用与清理边界
弱引用
允许观察对象而不靠该弱引用延长其寿命,适合一些缓存和观察者。弱引用不提供永久可用保证;取出后先用局部强引用保住,再检查是否为 None,不要分两次读取并假设结果相同。
显式管理资源
不可靠,执行时机、解释器退出、异常与对象复活都可能影响它。文件、锁、数据库连接优先用 with 或明确 close,垃圾回收只能作部分兜底,不能承担事务提交和业务成功确认。
闭包、异常与生成器保活
闭包 cell/defaults 可持有对象;保存 traceback 可保留帧及其局部对象;暂停的生成器/协程可保留执行状态。不是函数已返回就万事清理,排查要找真正持有引用的链路及释放时机。
缓存的生命周期
缓存会强引用键和值,maxsize=None 可无界增长,有限条数也可能每条很大;缓存实例方法时键还可能持有 self。按调用模式与每条大小设预算,失效和生命周期明确,别以「命中率高」证明不会泄漏。
对象复活
终结器可把 self 存入其他活容器,让对象重新可达;不要把终结器已跑当成对象已经彻底死亡。终结顺序与复活后再次终结的规则有实现差异,资源释放尽量幂等且不依赖全局模块仍完整存在。
finalize 的回调引用
绑定方法强引用 obj,finalize 持有回调,结果反而把目标保活。回调不应直接或间接引用目标,可使用独立资源句柄;仍优先明确 close/with,finalize 只是兜底,不负责可靠业务动作。
实验:不可达的引用环
# 可独立运行
import gc
import weakref
class Node:
pass
enabled = gc.isenabled()
try:
gc.disable()
node = Node()
node.link = node
reference = weakref.ref(node)
del node
assert reference() is not None
gc.collect()
assert reference() is None
finally:
if enabled:
gc.enable()实验不依赖精确引用计数或 GC 返回数量。对象回收后内存是否交还操作系统,需要继续读内存分配与观测。
