Python C扩展的引用计数问题探讨

简介:# Python GC机制 对于Python这种高级语言来说,开发者不需要自己管理和维护内存。Python采用了引用计数机制为主,标记-清除和分代收集两种机制为辅的垃圾回收机制。 首先_ p 7 U,需要搞清楚变量和对象的关系:
变量:通过变量指针引用对象。变量指针指向具体对象的内存空间,取对象的值。
对象,类型已知,T Z n 5 U 7 ? q R每个对象都% * / x M L 1 ^包含一个头部信息(头部信息:类型标识符和引用计数器)

Python GC机制

对于Python这种高级语言来说,开发者不需要自己管理和维护内存。Python采用了引用计数机制为主,标记-清除和分代收集两种机制为辅的垃圾回收机制。$ ] s [ # U

首先,需要搞清楚变量和对象的关系:

  • 变量:通过变量指针引用对象。( Z Z C ) : M [变量指针指向具体对象的内存空间,取对象的值。
  • 对象,类型已知,每个对象都包含一个头部信息(头部信息:类型标识符和引用计数器)

Python C扩展的引用计数问题探讨

引用计数

python里每一个东西都是对象,它们的核心就是一个结构体:PyObjectr ! C N N ?,其中ob_refcnt就是引用计数。当一个对象有新的引用时,ob_refcnt就会增加,当引用它的对象被删除,ob_refcnt就会减少。当引用计数为0时,该对象生命就结束M 9 了。

typedef struct_object {

int ob_refcnt;

struct_typeobject *ob_type;

} PyObject;

#defy K ) 3ine2 s ; W 8 % ~ Py_INCREF(op) ((op)->obt K 3 8 i F E_refcnt++) //增加计数

#define Py_DECREF(op) \ //减少计| ^ X o G

if (--(op)->ob_refcnt != 0) \

; \

else \

__Py_Dealloc((E L @ 2 w SPyObject *)(op))

W 6 { K ~ ;以使用s^ { $ lys.getrefcount()函数获取对象的引用计数,需要注意的是,使用时会比预期的引用次数多14 8 ^ C : = x X,原因是调用时会针对于查询的对象自动产生一个临时引用。

下面简单展现一下引用计数的变化过程。

  • 一开始8 f 6 3 t 4 a创建3个对象,引用计数分别是1。S s ; ~ &
  • 之后将n1指向了新的对象"JKL",则之前的对c B U象“ABC”的引用计数就变成0了。这时候,Python的垃圾l O H ^ t Z e + !回收器开始工作,将“ABC”释放。
  • 接着,让n2引用n1。“DEF”不再被引用,“JKL”因为被V F e in1、n2同时引用,所以引用计数变成了2。

Python C扩展的引用计数问题探讨

>>> n1 = "ABC"

>>> n2 = "DEF"

>>> n3 = "GHI"

>>> sys.getrefcount(n1)

2

>>> sys.getrefcount(n2)

2

>>> sys.getrefcount(n[ S , l = H3)

2

>>> n1 = "JKL"

>>> sys.getrefcount(n1)

2

>>> n2 = n1

>>> sys.getrefcount(n1)

3

>>> sys._ 6 E = A % l Vgetrefcount(n2)

3

>>> sysb ; r Z N - N Z r.getrefcount(n3)

2

缺点

优点:实时性好。一旦没有引用,内存就直接释放了。实时+ 7 % . K ) + @性还带来一个好处:处理回收内存的时间分摊到了平时。

缺点:维护引用计数消[ O 9 G耗资源;循环引用无法解决。

如下图,典型的循环引用场景。对象除了被变量引用n1、n2外,还被对方的prev或next指针引用,造成了引用计数为2。之后n1、n2设成null之后,引用计数仍然为1,导致对象无法被回收。

Python C扩展的引用计数问题探讨

标记-清除、分代收集

Pytho/ K on采用标记-清除策略来解决循环引用的7 u 7问题。但是该机制会导致应用程序卡住,为了减少程序暂停的时间,又通过“分代回收”(Generational Collection)以空间换时间的方法提高垃圾回收效率。详见Python垃圾回收机制!非常实用

Python C扩展的引用计数

Python提供了GC机制,保证对; u G 1象不被使用的时候会被释放掉,开发者不需要I 5 /过多关心内存管理的问题。但是当使用C扩展的时候,就不这么? 8 V h ~简单m [ 8 2了,必须需要理解CPython的引用计数。

当使用C扩展使用Python时,引用计数会随着PyObjects的创建自动加1,但是当释放该PyObjects的时候,我们需要显示的将PyObjects的引用计数减1,否则会出现内存泄漏。

#incI Z slude "Python.h"

void print_hello_world(void) {

PyObject *pObj = Ng Y o AULL;

pObj = PyBytes_FromString("Hello world\n"); /* Object creation, ref count = 1. */

PyObject_Print(pLast, stdout, 0);

Py_DECREF(pObj); /* ref count becomes 0, object deallocated.

* Miss this step and you have a memory leak. */

}

有亮点尤其需要注意:

  • PyObj* V N F X iects引用计数为0后,不能再访问。类似于C语言frep } ` ! . z u ,e后,不能再访问对象。
  • Py_INCREF、Py_DECREF必须成对出现。类似于C语言malloc、free的关系。

Python有三种引用形式,u . G Z $ @ . ?分别m r { 2 ~ o 8 K为 “New”, “Stolen” 和“Borrowed” 引用。

New引用

通过Python C Api创建出的PyObject,调用者对该PyObject具有完全的所有权C v X $。一般Python文档这样体现:

PyObject# a h (* PyList_New(int len)

Return value: New reference.

Returns a new list of length len on success, or NULL on fai! { : 3 | s c hlure.

针对于New引用的PyObject,有如下两种选择。否则,就会t V C出现内存泄漏。

  • 使v Z w @用完成后,调用Py_DECREF将其释放掉。

void MyCode(arguments) {

PyObject *pyo;

pyo = Py_Something(args);

Py_DECREF(pyo);

}

  • 将引用通过函数返回值等形式传递给上层调用函 y l 4 $数,但是接收者必须负责最终的Py_DECREF调用。

void MyCode(arguments) {

Pyh u F M | | ~ S !Object *pyo;

pyo = Py_Something(args);

return pyo;

}

使用样W o | E h 4 D H [例:

static@ i * PyObject *subtract_long(long a, long b) {

PyObject *pA, *pB, *r;

pA = PyLong_FromLong(a); /* pA: New refe# ` A ) k (rence. */

pB = PyLong_FromLong(b); /* pB: New reference. */

r = PyNumber_Subtract(pA, pB); /* r: New reference. */

Py9 ^ W v L_DECREF(pA); /* My responsibilit_ r * L Xy to decref. */

Py_DECREF(pB); /* My responsibility to decref. */ c k ; g ~

retuH # * Z 5 ? x prn r; /* Callers responsibility to decref. */

}

// 错误的例子,a、% K w [b两个PyObject泄漏k s J ~ i W B *

r = PyNumber_Subtract(PyLong_FromLong(a), PyLong_FromLong(b));

Stolen引用

当创建的PyObject传递给其他的容器,例如PyTuple_SetItem、PyList_SetItem。

static PyOb6 ^ $ R [ N r =ject *make_tuple(void) {

PyObject *r;

PyObject *v;

r = PyTuple_New(3); /* New referu [ c p o ? -ence. */

v = PyLoH ) 9 K ,ng_FromS . J eLong(1L); /* New reference. */

/* PyTuple_Set| i 3 6 C v @ iItem "steals"Y ~ u ] l i the new reference v. */

PyTuple_SetItem(r, 0, v);

/* This is fine. */

v = PyLong_FromLong(2L);

PyTuple_SetItem(# o ^ Z $r, 1, v);

/* More commo Y V h ` ^ k 7 hn pattern. */

PyTuple_SetItem(r, 2, PyUnicode_FromString("three"));

return r; /* Calle= p : L mrs responsibility to decref. */

}

但是,需要注意PyDict_SetItem内部会引用计数加一。

Borrowed引用

Python文档中,Borrowed引用的体现:

PyObjP q n [ # L q .ect* PyTuple_GeK o A OtItem(PyObject *p, Py_ssize_t pos)

Return value: Borrowed reference.

Borrowed 引用的所有者% : y c _ a % =不应该调用 Py_DECREF(),使用Borrowed 引用在函数退出时不会出现内/ , { q存泄露。。但是不要让一个对象处理未保护的状态Borrowed 引用,如果对象处理E 3 e 9 D f ~ b未保护状态,它随时可能j I n @ w n t | V会被销毁。例如:从一个 list 获取对象L _ j ,,继续操作它,但并不递增它的引用。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将m r J这个对象删除(递减它的引用计数,或者释放它),导致 item 成为一个悬垂指针。

bug(PyObject *list) {

PyObject *item = PyList_GetItem(list, 0);

PyList_SetItem(list, 1, PyInt_FromLong(e | ` z * 5 r e 00LT o ~ 7 + q 4));

PyObject_Print(item, stdout, 0); /* BUG! */

}

no_bug(PyObject *list) {

PyObject *item = PyList_GR _ x A netItem(list, 0);

Py_IT 3 8 - S W 2NCREF(item); /* Protu r 9 ) 9 X V ^ wect item. */

PyList_SetItem(list, 1, PyInt_FromLong(0L));

PyObjectY _ j # c # +_Print(item, stdout* X 6 T, 0);

Py_DECREF(item);

}

作者` h:烨陌

本文为阿里云原创内容,未经允许不得转载。