CPython 的内存管理主力是引用计数——每个对象头里有个计数器,记着"现在有多少个地方在指着我",减到 0 立刻释放,不用等任何后台线程。这一篇本机真实验证了这个"立刻"到底有多快,顺手挖到一个真实的、大多数教程不会讲的细节——某些对象的引用计数会被故意设成一个天文数字,永远不会归零;还真实复现了引用计数机制天生的一个漏洞:循环引用,以及 Python 怎么用另一套机制兜底。
不同语言给了完全不同的答案。
CPython 给每个对象头里塞了一个计数器,创建一个新引用 +1,引用消失 -1,减到 0 的瞬间立刻释放——不需要暂停程序去扫描整个堆,内存回收的时机是完全确定的。
如果 A 引用 B、B 又引用 A,就算外部谁都不再引用它俩,它们互相的引用还在,计数永远到不了 0——纯引用计数拿这种情况没办法。
Python 额外维护了一套专门找循环引用的收集器,定期扫描"从根节点出发够不着"的对象,不管它们的引用计数是不是大于 0,照样回收——引用计数管日常,循环检测管漏网之鱼。
每个 Python 对象的 C 结构体第一个字段就是引用计数。3.14.7 这个版本,标准版(带 GIL)和自由线程版的字段布局完全不一样。
Include/object.h · cpython @ v3.14.7, L110 #ifndef Py_GIL_DISABLED struct _object { union { PY_INT64_T ob_refcnt_full; struct { uint32_t ob_refcnt; // 引用计数,32 位 uint16_t ob_overflow; uint16_t ob_flags; }; }; PyTypeObject *ob_type; }; #else // 自由线程版:本地计数 + 共享(原子)计数分开存,减少多线程下的原子操作竞争 struct _object { uintptr_t ob_tid; uint32_t ob_ref_local; // 只属于当前线程的本地引用计数 Py_ssize_t ob_ref_shared; // 跨线程共享的原子引用计数 PyTypeObject *ob_type; }; #endif
本机装的是标准版(上一篇验证过 sys._is_gil_enabled() == True),用的是上半段那个结构。Py_INCREF/Py_DECREF 最终都是对 ob_refcnt 这个字段做加减:
Include/refcount.h · cpython @ v3.14.7, L252 static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op) { ... PY_UINT32_T cur_refcnt = op->ob_refcnt; if (cur_refcnt >= _Py_IMMORTAL_INITIAL_REFCNT) { // the object is immortal return; // 不朽对象,直接跳过,连加法都不做 } op->ob_refcnt = cur_refcnt + 1; ... }
这里出现了一个此前没提过的概念:_Py_IMMORTAL_INITIAL_REFCNT——"不朽对象"。
None、True/False、-5~256 的小整数、空元组这些高频用到的单例对象,真实引用计数测出来是这样的:
import sys
print('None:', sys.getrefcount(None))
print('True:', sys.getrefcount(True))
print('小整数 5:', sys.getrefcount(5))
print('大整数 99999999999:', sys.getrefcount(99999999999))
print('空元组:', sys.getrefcount(()))
None: 3221225472
True: 3221225472
小整数 5: 3221225472
大整数 99999999999: 3
空元组: 3221225472
不是溢出、不是 bug——3221225472 正是 3ULL << 30,精确对上真实源码里的常量:
Include/refcount.h · cpython @ v3.14.7, L49 /* In order to offer sufficient resilience to C extensions using the stable * ABI compiled against 3.11 or earlier, we set the initial value near the * middle of the range (2**31, 2**32). That way the refcount can be * off by ~1 billion without affecting immortality. */ #define _Py_IMMORTAL_INITIAL_REFCNT (3ULL << 30)
这是 Python 3.12 引入的"不朽对象"(PEP 683)优化:None/True/小整数这类对象在真实程序里会被疯狂引用又释放(几乎每次条件判断、每次小整数运算都会摸一下),给它们做真实的加减法是纯粹的浪费——干脆把初始引用计数设成一个大到几乎不可能自然到达、又留了足够安全余量的数,Py_INCREF 检测到这个值直接跳过不做加法,Py_DECREF 同理跳过减法。源码注释里那句"离中间值有意留了 10 亿的余量",是专门为了兼容 3.11 之前编译、还不知道"不朽"这回事的 C 扩展——就算它们对这类对象错误地做了几亿次减法,也不会把计数真的减到 0。99999999999 这种不在小整数缓存范围(-5~256)里的大整数,是真实的普通对象,引用计数就是正常的个位数。
用 __del__ 当探针,验证"计数到 0 就立刻释放"这件事,不是"最终会被释放"那种模糊承诺。
__del__ 的打印紧跟在 del t 后面同步出现,中间没有任何延迟——这和 Java、Go 那种"对象啥时候真的被回收不确定,finalize 可能很久之后才跑,甚至可能不跑"的模型完全不同。
关掉自动垃圾回收(gc.disable()),纯靠引用计数,构造一个真实的循环引用。
del a; del b 之后,两个对象的 __del__ 都没有触发——用 gc.get_objects() 扫描真实堆,两个节点确实还活着。只有手动跑一次 gc.collect()(真实收了 114 个对象,包括之前运行过程中积累的其他垃圾),__del__ 才终于打印出来,而且是先 B 后 A——收集器不看引用计数,看的是"从根节点还能不能走到"。
本机真实配置(默认值):
三代对象分别记数:新创建的对象先进 0 代,0 代积累的对象数超过阈值(本机是 2000)就触发一次 0 代扫描,活下来的对象晋升到 1 代;1 代超过阈值(10 次 0 代扫描没被晋升掉)触发一次 1/0 代联合扫描,以此类推。这个策略基于一个很朴素的假设——大多数对象活得很短,老对象很少变成垃圾,没必要每次都把所有对象重新扫一遍。
Python/gc.c · cpython @ v3.14.7, L1254 /* Find the oldest generation (highest numbered) where the count * exceeds the threshold. Objects in the that generation and * generations younger than it will be collected. */ static int gc_select_generation(GCState *gcstate) { for (int i = NUM_GENERATIONS-1; i >= 0; i--) { if (gcstate->generations[i].count > gcstate->generations[i].threshold) { // 额外还有一条启发式规则:只有"长期存活的待处理对象"占 // "长期存活总对象"的比例超过 25%,才会真的触发最老一代 // (最贵)的全量扫描——避免对象数一多,全量扫描的开销跟着 // 平方增长。 ... } } }
这条 25% 的启发式规则是 2008 年从 python-dev 邮件列表里的一次真实讨论定下来的(源码注释里直接留了当时的分析链接)——目的是避免"每积累固定数量的新对象就做一次全量扫描"这种朴素策略在长期存活对象很多的场景下,扫描开销随对象总数平方级增长。
复现上面两个真实实验:先是普通的引用计数增减(X 计数归零立刻释放),然后是一个真实的循环(A/B 互相引用),最后一次 GC 扫描把它们找出来。
数据来自独立验证过的 Python 模拟:每一次 dealloc 都核对过触发时刻的引用计数确实是 0;GC 扫描阶段找出的"不可达对象"用一套独立的可达性分析重新算过一遍,确认和演示里标记的完全一致。