Cpython的引用计数
Yongtao_Huang
编辑于 2026年01月17日 22:09
收录于文集
共2篇

这是我在cpython的第一行代码:https://github.com/python/cpython/pull/143131

引用计数是什么

CPython 的"引用计数(reference count)"就是:一个对象当前被多少个地方"持有"的计数。

我们可以使用python的Lib函数 sys.getrefcount() sys.gettotalrefcount() 来观察引用计数值。

  • sys.getrefcount():返回某一个对象当前的引用计数。

  • sys.gettotalrefcount():返回 整个 CPython 进程中,所有对象的引用计数总和。

代码块
Python
自动换行
复制代码
import sys

a = []
print(sys.getrefcount(a))       # 2
b = a
print(sys.getrefcount(a))       # 3
print(sys.gettotalrefcount())   # 20403
复制成功

返回值 比真实引用数大 1。因为 a 被作为参数传给 getrefcount(),临时多持有了一次引用。

当前运行环境,整个 CPython 进程的引用计数为 20403 。

引用计数常用的5个函数

  • Py_INCREF(PyObject *o):把 o 的引用计数 +1。

  • Py_DECREF(PyObject *o):把 o 的引用计数 -1;如果变成 0,会触发析构/释放。

  • Py_XDECREF(PyObject *o):o 为 NULL 就什么也不做;否则等价于 Py_DECREF(o)。能处理 NULL 的 Py_DECREF(o) 。

  • Py_XINCREF(PyObject *o):o 为 NULL 就什么都不做;否则等价于 Py_INCREF(o)。能处理 NULL 的 Py_INCREF(o)。

  • Py_NewRef(PyObject *o):返回 o,并且对 o 做一次 INCREF。也就是:"把借用引用变成新引用" 的常用写法。

以上函数都定义在 Include/refcount.h

引用计数和内存泄漏之间的联系

引用计数归零 => 对象不可达 => 立刻析构。

核心就在于函数 Py_DECREF(),如果引用计数该归零的时候没归零,导致该析构的时候没有析构,就会导致内存泄漏。

可以类比 C语言 的 堆内存分配 使用了 calloc() 但是忘记使用 free()

代码块
clike
自动换行
复制代码
static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
    // ...
    if (--op->ob_refcnt == 0) { // 引用计数--。
        _Py_Dealloc(op);        // 引用计数归0,立刻析构。
    }
}
复制成功

实战PR

回到我的PR:https://github.com/python/cpython/pull/143131

那我们需要构造一个能够触发到此处的Python脚本来验证它,通过让函数走到这个路径里,观测整体的 sys.gettotalrefcount() 是否存在线性增长只增不减的情况。

代码块
Python
自动换行
复制代码
import ctypes
import sys
import gc

class BadInt(ctypes.c_int):
    def __ctypes_from_outparam__(self):
        raise RuntimeError("boom from __ctypes_from_outparam__")

msvcrt = ctypes.CDLL("msvcrt.dll")
PROTO = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(BadInt), ctypes.POINTER(BadInt), ctypes.c_size_t)

PARAMFLAG_FIN = 1
PARAMFLAG_FOUT = 2
paramflags = (
    (PARAMFLAG_FOUT, "a", BadInt()),
    (PARAMFLAG_FOUT, "b", BadInt()),
    (PARAMFLAG_FIN,  "n"),
)
f = PROTO(("memmove", msvcrt), paramflags)

def totalrefcount():
    return getattr(sys, "gettotalrefcount")()

base = totalrefcount()
print("base totalrefcount:", base)

N = 20000
step = 2000

for i in range(1, N + 1):
    try:
        f(4)
    except RuntimeError:
        pass
    if i % step == 0:
        gc.collect()
        cur = totalrefcount()
        print(f"{i} totalrefcount: {cur}  delta: {cur - base}")
复制成功

我是在Windows环境下编译运行的,没打补丁的结果是:

代码块
Python
自动换行
复制代码
d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_leak_outparam_tuple.py
base totalrefcount: 26086
2000 totalrefcount: 27922  delta: 1836
4000 totalrefcount: 29923  delta: 3837
6000 totalrefcount: 31923  delta: 5837
8000 totalrefcount: 33923  delta: 7837
10000 totalrefcount: 35923  delta: 9837
12000 totalrefcount: 37923  delta: 11837
14000 totalrefcount: 39923  delta: 13837
16000 totalrefcount: 41923  delta: 15837
18000 totalrefcount: 43923  delta: 17837
20000 totalrefcount: 45923  delta: 19837
复制成功

确实,引用计数一直在稳定增加,这些就是不该持有的对 tup 的引用。修复方式也很简单,对 tup 的引用计数-1: Py_XDECREF(tup);

参考和推荐

主要的参考书目还是《CPython设计与实现》,类似的修复还有很多,比如如下PR:

  • https://github.com/python/cpython/pull/142492

  • https://github.com/python/cpython/pull/140908

NEXT?

什么是新引用? 什么是借用引用? 我一直没有弄懂。