cpython C API 引用计数机制详解:Py_INCREF、Py_DECREF、Py_CLEAR 与 Immortal 对象
本文基于 cpython 仓库的 C API 文档 Doc/c-api/refcounting.rst,完整讲解 Python C 扩展中用于管理对象引用计数的全部函数与宏:从读取/设置引用计数(Py_REFCNT/Py_SET_REFCNT),到增删强引用(Py_INCREF/Py_DECREF 及其 NULL 安全变体),再到现代写法 Py_NewRef 与安全释放宏 Py_CLEAR/Py_SETREF。读完后你不仅能正确使用这些 API 编写 C 扩展,还能从 Include/refcount.h 的源码层面理解 immortal 对象优化与 free-threaded 构建下的分片引用计数实现。
一、引用计数 API 总览
cpython 使用引用计数(reference counting)作为其主要的内存回收机制之一:每当一个对象被新增一个强引用,引用计数加一;每释放一个强引用,计数减一;计数归零时对象类型的 dealloc 函数被调用,对象内存随即释放。C API 为此提供了一组函数和宏,全部在 Doc/c-api/refcounting.rst 中有定义:
| API | 类型 | 作用 | 引入版本 |
|---|---|---|---|
Py_REFCNT(o) |
内联函数 | 获取对象 o 的引用计数 |
3.10 起改为内联函数(更早为宏) |
Py_SET_REFCNT(o, refcnt) |
函数 | 将对象 o 的引用计数设为 refcnt |
3.9 |
Py_INCREF(o) |
内联函数/宏 | 为对象 o 增加一个强引用 |
历史 API |
Py_XINCREF(o) |
内联函数/宏 | 同 Py_INCREF,但 o 可为 NULL |
历史 API |
Py_NewRef(o) |
内联函数/函数 | 创建新强引用并返回对象本身 | 3.10 |
Py_XNewRef(o) |
内联函数/函数 | 同 Py_NewRef,但 o 可为 NULL |
3.10 |
Py_DECREF(o) |
内联函数/宏 | 释放一个强引用 | 历史 API |
Py_XDECREF(o) |
内联函数/宏 | 同 Py_DECREF,但 o 可为 NULL |
历史 API |
Py_CLEAR(o) |
宏 | 释放 o 的强引用并将其置为 NULL |
历史 API |
Py_IncRef(o) |
函数 | Py_XINCREF 的函数版本,用于运行时动态嵌入 |
历史 API |
Py_DecRef(o) |
函数 | Py_XDECREF 的函数版本,用于运行时动态嵌入 |
历史 API |
Py_SETREF(dst, src) |
宏 | 安全释放 dst 并设为 src |
3.6 |
Py_XSETREF(dst, src) |
宏 | Py_SETREF 的 Py_XDECREF 变体 |
3.6 |
理解这组 API 的前提是区分两个术语(见 Doc/glossary.rst):强引用(strong reference) 持有对象并阻止其被回收;借用引用(borrowed reference) 不增加计数,只保证在对象生命周期内有效,常见于 C API 函数返回值(如 PyTuple_GET_ITEM)和类型槽函数的部分参数。将借用引用转为强引用的典型方式就是 Py_INCREF。
二、读取与设置引用计数:Py_REFCNT 与 Py_SET_REFCNT
Py_REFCNT:读取引用计数
Py_ssize_t Py_REFCNT(PyObject *o) 返回对象 o 的引用计数。官方文档给出了两条重要的使用告诫:
- 返回值不一定反映真实引用数。一些对象是 immortal(永生对象,见下文),其引用计数是一个很大的特殊值,并不代表实际被引用了多少次。因此除 0 和 1 之外,不要依赖该值的准确性;
- free-threaded 构建下,读到 1 不代表线程安全。在 free-threaded build 中,返回 1 并不能保证没有其他线程正在访问对象,判断"是否可安全独占修改"应改用
PyUnstable_Object_IsUniquelyReferenced,参见 Doc/howto/free-threading-python.rst。相关声明位于 Include/cpython/object.h:
PyAPI_FUNC(int) PyUnstable_Object_IsUniqueReferencedTemporary(PyObject *);
PyAPI_FUNC(int) PyUnstable_Object_IsUniquelyReferenced(PyObject *);
版本演进:3.10 起 Py_REFCNT 由宏改为 static inline 函数;3.11 起参数类型不再是 const PyObject*。
从源码看,非稳定 ABI 构建中它就是一个内联读取(Include/refcount.h):
static inline Py_ssize_t _Py_REFCNT(PyObject *ob) {
#if !defined(Py_GIL_DISABLED)
return ob->ob_refcnt;
#else
uint32_t local = _Py_atomic_load_uint32_relaxed(&ob->ob_ref_local);
if (local == _Py_IMMORTAL_REFCNT_LOCAL) {
return _Py_IMMORTAL_INITIAL_REFCNT;
}
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&ob->ob_ref_shared);
return _Py_STATIC_CAST(Py_ssize_t, local) +
Py_ARITHMETIC_RIGHT_SHIFT(Py_ssize_t, shared, _Py_REF_SHARED_SHIFT);
#endif
}
在 free-threaded 构建(Py_GIL_DISABLED)下,引用计数被拆分为本地计数(当前线程私有,ob_ref_local)与共享计数(其余线程,ob_ref_shared,低 2 位存标志位),Py_REFCNT 返回两者之和——这正是"读到的计数可能随其他线程变动而不精确"的底层原因。稳定 ABI(limited C API 3.14+ 或 abi3t)下则通过导出函数 Py_REFCNT(PyObject*) 调用(Include/refcount.h)。
Py_SET_REFCNT:设置引用计数
void Py_SET_REFCNT(PyObject *o, Py_ssize_t refcnt)(3.9 引入)将对象 o 的引用计数设为 refcnt。两个关键行为:
- free-threaded 构建:若
refcnt大于UINT32_MAX,对象会被置为 immortal; - immortal 对象不受影响(3.12 起):对永生对象调用此函数没有任何效果。
Include/refcount.h 的实现印证了这一点:
static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
assert(refcnt >= 0);
// ...
if (_Py_IsImmortal(ob)) {
return; // immortal 对象直接忽略
}
#if !defined(Py_GIL_DISABLED)
ob->ob_refcnt = (uint32_t)refcnt; // 64 位系统只写低 32 位
#else
// free-threaded:若持有线程则写本地计数(溢出则置 immortal),
// 否则清零本地计数、把目标值写入共享计数并标记 MERGED
#endif
}
三、Immortal 对象:为什么引用计数会"失真"
3.12 起,Py_INCREF/Py_DECREF/Py_SET_REFCNT 对 immortal 对象一律不做任何修改(文档中标注为 3.12 的 versionchanged)。immortal 机制的位级策略完整记录在 Include/refcount.h 的注释中,核心结论是:
-
64 位系统:引用计数低 32 位 ≥ 231 的对象视为 immortal。初始 immortal 值定为
_Py_INCREF_INITIAL_REFCNT = 3 << 30(即 3.2 亿,处于 (231, 2**32) 区间中部),这样即使针对 3.11 或更早版本编译的 C 扩展用饱和加法"多减"约 10 亿次也不会跌破 immortal 阈值——这是向后兼容性的关键设计:#define _Py_IMMORTAL_INITIAL_REFCNT (3ULL << 30) #define _Py_IMMORTAL_MINIMUM_REFCNT (1ULL << 31)判断 immortal 只需检查低 32 位的符号位(Include/refcount.h):
static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op) { #if defined(Py_GIL_DISABLED) return (_Py_atomic_load_uint32_relaxed(&op->ob_ref_local) == _Py_IMMORTAL_REFCNT_LOCAL); #elif SIZEOF_VOID_P > 4 return _Py_CAST(int32_t, op->ob_refcnt) < 0; // 64 位:符号位判断 #else return op->ob_refcnt >= _Py_IMMORTAL_MINIMUM_REFCNT; // 32 位:阈值比较 #endif } -
32 位系统:引用计数 ≥ 2**30 视为 immortal,初始值
5 << 28; -
free-threaded 构建:用固定的 32 位常量
UINT32_MAX(_Py_IMMORTAL_REFCNT_LOCAL)标记本地计数字段。
这套机制的动机是避免对 None、True、False、小整数等高频单例做无谓的原子增减。对扩展作者的实际含义:看到巨大的 Py_REFCNT 值不要惊讶,也不需要"补偿"它。
四、增加强引用:Py_INCREF、Py_XINCREF 与 Py_NewRef
Py_INCREF / Py_XINCREF
void Py_INCREF(PyObject *o) 表示你对对象 o 持有一个新的强引用,表明它正在被使用、不应被销毁。文档明确了几条语义:
- 对 immortal 对象无效果(3.12 起);
- 通常用于将借用引用就地转换为强引用;若希望"创建新引用",推荐
Py_NewRef; - 用完后调用
Py_DECREF释放; - 参数不得为
NULL,不确定是否为NULL时用Py_XINCREF(对NULL无操作); - "不要指望该函数真的以某种方式修改
o"——至少对 PEP 683 覆盖的那类对象(单例),它什么都不做。
常规构建下的实现极其简单(Include/refcount.h):先做 immortal 检查,否则 ob_refcnt++。free-threaded 构建则区分"本线程持有"(直接改本地计数)与"跨线程持有"(原子地给共享计数加 1),immortal 对象直接返回。
Py_NewRef / Py_XNewRef(3.10+)
PyObject* Py_NewRef(PyObject *o) 创建对象的新强引用:对 o 执行 Py_INCREF 并返回 o 本身。当强引用不再需要时,应调用 Py_DECREF 释放它。o 不得为 NULL;可能为 NULL 时用 Py_XNewRef(对 NULL 直接返回 NULL)。
文档给出的改写示例展示了它的现代价值:
// 旧写法
Py_INCREF(obj);
self->attr = obj;
// 新写法(更不易漏写/错写顺序)
self->attr = Py_NewRef(obj);
源码层面,非稳定 ABI 下 Py_NewRef 就是内联的 "INCREF + return"(Include/refcount.h):
static inline PyObject* _Py_NewRef(PyObject *obj)
{
Py_INCREF(obj);
return obj;
}
而在 Objects/object.c 中,Py_NewRef/Py_XNewRef 被作为普通导出函数提供给稳定 ABI(stable ABI 无法使用 static inline):
PyObject*
Py_NewRef(PyObject *obj)
{
return _Py_NewRef(obj);
}
五、释放强引用:Py_DECREF、Py_XDECREF、Py_CLEAR
Py_DECREF / Py_XDECREF
void Py_DECREF(PyObject *o) 释放对象 o 的一个强引用,表示该引用不再使用。语义要点(与文档一致):
- 对 immortal 对象无效果(3.12 起);
- 当最后一个强引用被释放(计数归零)时,调用该对象类型的 dealloc 函数(不得为
NULL); - 通常用于在作用域退出前删除一个强引用;
- 参数不得为
NULL,可能为NULL时用Py_XDECREF。
常规构建的核心路径只有两行(Include/refcount.h):
static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
if (_Py_IsImmortal(op)) {
return;
}
if (--op->ob_refcnt == 0) {
_Py_Dealloc(op);
}
}
free-threaded 构建则复杂得多(Include/refcount.h):本线程持有时先减本地计数,本地计数归零后调用 _Py_MergeZeroLocalRefcount 与共享计数合并,由"拥有者线程"统一判断是否真正销毁;非本线程持有则走 _Py_DecRefShared 原子地操作共享计数。相关销毁判定逻辑可进一步参考 Objects/object.c 中的 _Py_DecRefSharedIsDead。
文档中的 warning 是扩展开发中最值得细读的一段:
dealloc 函数可能触发任意 Python 代码的执行(例如带
__del__方法的类实例被销毁时)。虽然其中的异常不会向外传播,但这些代码可以自由访问所有 Python 全局变量。这意味着:在调用Py_DECREF之前,任何可从全局变量到达的对象都应处于一致状态。例如,从列表中删除一个对象的代码应当先把被删对象的引用拷贝到临时变量、更新列表数据结构、然后再对临时变量调用Py_DECREF。
换句话说,释放一个引用可能同步执行一段任意 Python 代码,你的局部状态必须对此免疫。
Py_CLEAR:先置空再释放
void Py_CLEAR(PyObject *o) 释放 o 的强引用;若 o 本身是 NULL 则无操作;否则效果同 Py_DECREF,但参数会被置为 NULL。关键区别在于实现顺序——它使用临时变量,先把参数置为 NULL,再释放引用,因此 Py_DECREF 的那条 warning 对传入对象本身不再适用。文档建议:凡是在垃圾回收期间可能被遍历的对象上释放引用,都应使用此宏。3.12 起宏参数只被求值一次,副作用不会被重复执行。
Include/refcount.h 中大段注释解释了为什么"显而易见"的写法是致命的:
/* 危险写法:
* Py_XDECREF(op);
* op = NULL;
* 若 op 是 refcount 为 1 的 self->containee,第一行就把它销毁,
* 而销毁过程可触发 __del__、weakref 回调、释放 GIL 让其他线程运行,
* 甚至再次调用 self 的方法或触发循环 GC——
* 但此时 self->containee 仍指向正在销毁的对象,可能已处于荒谬状态。
*
* 安全写法:Py_CLEAR(op);
* 先置 NULL 再 decref,任何由销毁引发的代码都不再认为 op 指向有效对象。
*/
#define Py_CLEAR(op) \
do { \
_Py_TYPEOF(op)* _tmp_op_ptr = &(op); \
_Py_TYPEOF(op) _tmp_old_op = (*_tmp_op_ptr); \
if (_tmp_old_op != NULL) { \
*_tmp_op_ptr = _Py_NULL; \ /* 先置空 */ \
Py_DECREF(_tmp_old_op); \ /* 再释放 */ \
} \
} while (0)
注释还特别指出了两个工程细节:gh-98724 让宏参数只求值一次;gh-99701 指出若用 memcpy 实现可能因 strict aliasing 下的类型双关(type punning)被编译器错误优化,因此实现优先使用 _Py_TYPEOF 声明与 op 同类型的临时变量,不可用时退回 memcpy 做类型擦除。
六、Py_SETREF / Py_XSETREF:安全替换目标指针
Py_SETREF(dst, src)(3.6 引入)是"安全释放 dst 的强引用并把 dst 设为 src"的宏。文档再次强调"显而易见"的代码可能是致命的:
// 危险:
Py_DECREF(dst);
dst = src;
// 安全:
Py_SETREF(dst, src);
它先让 dst 指向 src,再释放 dst 旧值的引用,这样旧值销毁过程中触发的任何代码都不会再误以为 dst 指向一个有效对象。Py_XSETREF(dst, src) 是其使用 Py_XDECREF 而非 Py_DECREF 的变体(3.6 引入)。两者在 3.12 起参数均只被求值一次。
Include/cpython/object.h 中的实现与 Py_CLEAR 同源同构(同样有 _Py_TYPEOF 与 memcpy 两套版本):
#define Py_SETREF(dst, src) \
do { \
_Py_TYPEOF(dst)* _tmp_dst_ptr = &(dst); \
_Py_TYPEOF(dst) _tmp_old_dst = (*_tmp_dst_ptr); \
*_tmp_dst_ptr = (src); \ /* 先赋值 */ \
Py_DECREF(_tmp_old_dst); \ /* 后释放旧值 */ \
} while (0)
注意 Py_SETREF 与 Py_CLEAR 的组合覆盖了两类常见场景:Py_CLEAR 用于"释放后不再持有"(典型于 tp_clear/tp_dealloc),Py_SETREF 用于"用新值替换旧值"。
七、函数版本:Py_IncRef 与 Py_DecRef(运行时动态嵌入)
void Py_IncRef(PyObject *o) 与 void Py_DecRef(PyObject *o) 分别是 Py_XINCREF 与 Py_XDECREF 的函数版本,文档指出其用途是"运行时动态嵌入 Python"(runtime dynamic embedding)——即程序在运行期才 LoadLibrary/dlopen 加载 libpython 的场景:此时宏展开依赖的 PyObject 结构体布局、编译期标志等在动态加载下并不可用,而真实函数符号的调用只依赖 ABI 约定的函数签名,可保证对象码不依赖于 Python 的编译选项。
Objects/object.c 中的实现印证了这一对应关系:
void
Py_IncRef(PyObject *o)
{
Py_XINCREF(o);
}
void
Py_DecRef(PyObject *o)
{
Py_XDECREF(o);
}
// 不接收 NULL 的内部版本,被 Py_INCREF()/Py_DECREF() 稳定 ABI 路径调用
void
_Py_IncRef(PyObject *o)
{
Py_INCREF(o);
}
void
_Py_DecRef(PyObject *o)
{
Py_DECREF(o);
}
注意 Py_IncRef/Py_DecRef 接受 NULL(等价于 X 变体),而 _Py_IncRef/_Py_DecRef 不接受 NULL。
八、稳定 ABI 与版本演进小结
从源码结构看,这一组 API 在不同构建形态下有系统性的实现分叉(均以 Include/refcount.h 的条件编译为准):
- 稳定 ABI(Py_LIMITED_API):
Py_INCREF/Py_DECREF在 limited C API 3.12+ 或 debug 构建下实现为函数调用(走_Py_IncRef/_Py_DecRef,3.10.0a7 之前退回Py_IncRef/Py_DecRef);Py_REFCNT在 limited C API 3.14+(及 abi3t)下也是函数调用;Py_SET_REFCNT在 limited C API 3.13+ 下走导出函数_Py_SetRefcnt;Py_NewRef/Py_XNewRef则始终以导出函数形式提供(Objects/object.c)。 - 调试构建(Py_REF_DEBUG):
Py_DECREF展开为带__FILE__/__LINE__参数的重载版本,可对已释放对象或负计数调用_Py_NegativeRefcount触发诊断(Include/refcount.h),并用_Py_INCREF_IncRefTotal/_Py_DECREF_DecRefTotal维护全局增删引用统计——写 C 扩展时这是排查引用计数泄漏的首选构建。
按文档中的版本标注整理的时间线:
| 版本 | 变化 |
|---|---|
| 3.6 | 新增 Py_SETREF、Py_XSETREF |
| 3.9 | 新增 Py_SET_REFCNT |
| 3.10 | 新增 Py_NewRef、Py_XNewRef;Py_REFCNT 改为内联函数 |
| 3.11 | Py_REFCNT 参数类型不再是 const PyObject* |
| 3.12 | immortal 对象不再被 Py_INCREF/Py_DECREF/Py_SET_REFCNT/Py_CLEAR 修改;Py_CLEAR/Py_SETREF/Py_XSETREF 宏参数只求值一次 |
| 3.13/3.14 | Py_SET_REFCNT(3.13+)/Py_REFCNT(3.14+)在稳定 ABI 下改为函数调用 |
九、实践要点清单
- 区分引用所有权:拿到对象先判断是强引用还是借用引用;保存起来(放进结构体、容器)必须
Py_NewRef/Py_INCREF一份; - 可能为 NULL 就选 X 变体:
Py_XINCREF/Py_XDECREF/Py_XNewRef,避免对 NULL 解引用; - 清理成员用
Py_CLEAR,替换成员用Py_SETREF:二者都通过"先改指针、再释放旧值"的顺序消除销毁回调重入窗口; - 不要相信
Py_REFCNT的精确值(immortal 对象除外地判断 0/1);free-threaded 构建下判断独占引用请用PyUnstable_Object_IsUniquelyReferenced; - 释放引用前保持全局可达状态一致:
Py_DECREF可能同步执行__del__等任意 Python 代码,从容器删元素时先拷临时变量、改结构、再释放; - 动态嵌入 libpython 用
Py_IncRef/Py_DecRef,避免宏展开对结构体布局的依赖。
以上全部行为均可在当前仓库中核对:API 语义见 Doc/c-api/refcounting.rst,引用计数核心实现(immortal 常量、各变体内联实现、Py_CLEAR)见 Include/refcount.h,Py_SETREF/Py_XSETREF 见 Include/cpython/object.h,嵌入用函数与稳定 ABI 导出见 Objects/object.c,free-threaded 语义背景见 Doc/howto/free-threading-python.rst。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00