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

Python GC机制

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

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

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

引用计数

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

1typedef struct_object { 2 int ob_refcnt; 3 struct_typeobject *ob_type; 4} PyObject; 5 6#define Py_INCREF(op) ((op)->ob_refcnt++) //增加计数 7#define Py_DECREF(op) \ //减少计数 8 if (--(op)->ob_refcnt != 0) \ 9 ; \ 10 else \ 11 __Py_Dealloc((PyObject *)(op))

可以使用sys.getrefcount()函数获取对象的引用计数,需要注意的是,使用时会比预期的引用次数多1,原因是调用时会针对于查询的对象自动产生一个临时引用。

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

  • 一开始创建3个对象,引用计数分别是1。
  • 之后将n1指向了新的对象"JKL",则之前的对象“ABC”的引用计数就变成0了。这时候,Python的垃圾回收器开始工作,将“ABC”释放。
  • 接着,让n2引用n1。“DEF”不再被引用,“JKL”因为被n1、n2同时引用,所以引用计数变成了2。

1>>> n1 = "ABC" 2>>> n2 = "DEF" 3>>> n3 = "GHI" 4>>> sys.getrefcount(n1) 52 6>>> sys.getrefcount(n2) 72 8>>> sys.getrefcount(n3) 92 10>>> n1 = "JKL" 11>>> sys.getrefcount(n1) 122 13>>> n2 = n1 14>>> sys.getrefcount(n1) 153 16>>> sys.getrefcount(n2) 173 18>>> sys.getrefcount(n3) 192

优缺点:

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

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

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

标记-清除、分代收集

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

Python C扩展的引用计数

Python提供了GC机制,保证对象不被使用的时候会被释放掉,开发者不需要过多关心内存管理的问题。但是当使用C扩展的时候,就不这么简单了,必须需要理解CPython的引用计数。

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

1#include "Python.h" 2 3void print_hello_world(void) { 4 PyObject *pObj = NULL; 5 6 pObj = PyBytes_FromString("Hello world\n"); /* Object creation, ref count = 1. */ 7 PyObject_Print(pLast, stdout, 0); 8 Py_DECREF(pObj); /* ref count becomes 0, object deallocated. 9 * Miss this step and you have a memory leak. */ 10}

有亮点尤其需要注意:

  • PyObjects引用计数为0后,不能再访问。类似于C语言free后,不能再访问对象。
  • Py_INCREF、Py_DECREF必须成对出现。类似于C语言malloc、free的关系。

Python有三种引用形式,分别为 “New”, “Stolen” 和“Borrowed” 引用。

New引用

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

1PyObject* PyList_New(int len) 2 Return value: New reference. 3 Returns a new list of length len on success, or NULL on failure.

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

  • 使用完成后,调用Py_DECREF将其释放掉。

    void MyCode(arguments) { PyObject *pyo; ... pyo = Py_Something(args); ... Py_DECREF(pyo); } 将引用通过函数返回值等形式传递给上层调用函数,但是接收者必须负责最终的Py_DECREF调用。

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

    void MyCode(arguments) { PyObject *pyo; ... pyo = Py_Something(args); ... return pyo; }

使用样例:

1static PyObject *subtract_long(long a, long b) { 2 PyObject *pA, *pB, *r; 3 4 pA = PyLong_FromLong(a); /* pA: New reference. */ 5 pB = PyLong_FromLong(b); /* pB: New reference. */ 6 r = PyNumber_Subtract(pA, pB); /* r: New reference. */ 7 Py_DECREF(pA); /* My responsibility to decref. */ 8 Py_DECREF(pB); /* My responsibility to decref. */ 9 return r; /* Callers responsibility to decref. */ 10} 11 12// 错误的例子,a、b两个PyObject泄漏。 13r = PyNumber_Subtract(PyLong_FromLong(a), PyLong_FromLong(b));

Stolen引用

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

1static PyObject *make_tuple(void) { 2 PyObject *r; 3 PyObject *v; 4 5 r = PyTuple_New(3); /* New reference. */ 6 v = PyLong_FromLong(1L); /* New reference. */ 7 /* PyTuple_SetItem "steals" the new reference v. */ 8 PyTuple_SetItem(r, 0, v); 9 /* This is fine. */ 10 v = PyLong_FromLong(2L); 11 PyTuple_SetItem(r, 1, v); 12 /* More common pattern. */ 13 PyTuple_SetItem(r, 2, PyUnicode_FromString("three")); 14 return r; /* Callers responsibility to decref. */ 15}

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

Borrowed引用

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

1PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos) 2Return value: Borrowed reference.

Borrowed 引用的所有者不应该调用 Py_DECREF(),使用Borrowed 引用在函数退出时不会出现内存泄露。。但是不要让一个对象处理未保护的状态Borrowed 引用,如果对象处理未保护状态,它随时可能会被销毁。

例如:从一个 list 获取对象,继续操作它,但并不递增它的引用。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将这个对象删除(递减它的引用计数,或者释放它),导致 item 成为一个悬垂指针。

1bug(PyObject *list) { 2 PyObject *item = PyList_GetItem(list, 0); 3 PyList_SetItem(list, 1, PyInt_FromLong(0L)); 4 PyObject_Print(item, stdout, 0); /* BUG! */ 5} 6 7no_bug(PyObject *list) { 8 PyObject *item = PyList_GetItem(list, 0); 9 Py_INCREF(item); /* Protect item. */ 10 PyList_SetItem(list, 1, PyInt_FromLong(0L)); 11 PyObject_Print(item, stdout, 0); 12 Py_DECREF(item); 13}

原文链接

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

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java 复制Map对象(深拷贝与浅拷贝)

java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象

java常用类(2)

三、时间处理相关类Date类:计算机世界把1970年1月1号定为基准时间,每个度量单位是毫秒(1秒的千分之一),用long类型的变量表示时间。Date分配Date对象并初始化对象,以表示自从标准基准时间(称为“历元”(epoch),即1970年1月1日08:00:00GMT)以来的指定毫秒数。示例:packagecn.tanjian

[Dart]Dart语言之旅<二>:变量

变量以下是创建变量并为其分配值的示例:varname'Bob';变量是引用。名为name的变量包含对值为“Bob”的String类型的对象的引用。默认值未初始化的变量的初始值为null。即使是数字类型的变量,初始值也为null,因为数字也是对象。intlineCount;assert(lineCountnull)