
这是我在cpython的第一行代码:https://github.com/python/cpython/pull/143131
CPython 的"引用计数(reference count)"就是:一个对象当前被多少个地方"持有"的计数。
我们可以使用python的Lib函数 sys.getrefcount() 和 sys.gettotalrefcount() 来观察引用计数值。
sys.getrefcount():返回某一个对象当前的引用计数。
sys.gettotalrefcount():返回 整个 CPython 进程中,所有对象的引用计数总和。
import sys
a = []
print(sys.getrefcount(a)) # 2
b = a
print(sys.getrefcount(a)) # 3
print(sys.gettotalrefcount()) # 20403

返回值 比真实引用数大 1。因为 a 被作为参数传给 getrefcount(),临时多持有了一次引用。
当前运行环境,整个 CPython 进程的引用计数为 20403 。
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()。
static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
// ...
if (--op->ob_refcnt == 0) { // 引用计数--。
_Py_Dealloc(op); // 引用计数归0,立刻析构。
}
}
回到我的PR:https://github.com/python/cpython/pull/143131

那我们需要构造一个能够触发到此处的Python脚本来验证它,通过让函数走到这个路径里,观测整体的 sys.gettotalrefcount() 是否存在线性增长只增不减的情况。
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环境下编译运行的,没打补丁的结果是:
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
什么是新引用? 什么是借用引用? 我一直没有弄懂。