Python C扩展的引用计数问题探讨
Python GC机制
对于Python这种高级语言来说,开发者不需要自己管理和维护内存。Python采用了引用计数机制为主,标记-清除和分代收集两种机制为辅的垃圾回收机制。
首先,需要搞清楚变量和对象的关系:
- 变量:通过变量指针引用对象。变量指针指向具体对象的内存空间,取对象的值。
- 对象,类型已知,每个对象都包含一个头部信息(头部信息:类型标识符和引用计数器)
引用计数
python里每一个东西都是对象,它们的核心就是一个结构体:PyObject,其中ob_refcnt就是引用计数。当一个对象有新的引用时,ob_refcnt就会增加,当引用它的对象被删除,ob_refcnt就会减少。当引用计数为0时,该对象生命就结束了。
typedef struct_object { int ob_refcnt; struct_typeobject *ob_type; } PyObject; #define Py_INCREF(op) ((op)->ob_refcnt++) //增加计数 #define Py_DECREF(op) \ //减少计数 if (--(op)->ob_refcnt != 0) \ ; \ else \ __Py_Dealloc((PyObject *)(op))
可以使用sys.getrefcount()函数获取对象的引用计数,需要注意的是,使用时会比预期的引用次数多1,原因是调用时会针对于查询的对象自动产生一个临时引用。
下面简单展现一下引用计数的变化过程。
- 一开始创建3个对象,引用计数分别是1。
- 之后将n1指向了新的对象"JKL",则之前的对象“ABC”的引用计数就变成0了。这时候,Python的垃圾回收器开始工作,将“ABC”释放。
- 接着,让n2引用n1。“DEF”不再被引用,“JKL”因为被n1、n2同时引用,所以引用计数变成了2。
>>> n1 = "ABC" >>> n2 = "DEF" >>> n3 = "GHI" >>> sys.getrefcount(n1) 2 >>> sys.getrefcount(n2) 2 >>> sys.getrefcount(n3) 2 >>> n1 = "JKL" >>> sys.getrefcount(n1) 2 >>> n2 = n1 >>> sys.getrefcount(n1) 3 >>> sys.getrefcount(n2) 3 >>> sys.getrefcount(n3) 2
优缺点:
优点:实时性好。一旦没有引用,内存就直接释放了。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
缺点:维护引用计数消耗资源;循环引用无法解决。
如下图,典型的循环引用场景。对象除了被变量引用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,否则会出现内存泄漏。
#include "Python.h" void print_hello_world(void) { PyObject *pObj = NULL; 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. */ }
有亮点尤其需要注意:
- PyObjects引用计数为0后,不能再访问。类似于C语言free后,不能再访问对象。
- Py_INCREF、Py_DECREF必须成对出现。类似于C语言malloc、free的关系。
Python有三种引用形式,分别为 “New”, “Stolen” 和“Borrowed” 引用。
New引用
通过Python C Api创建出的PyObject,调用者对该PyObject具有完全的所有权。一般Python文档这样体现:
PyObject* PyList_New(int len) Return value: New reference. 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调用。
void MyCode(arguments) { PyObject *pyo; ... pyo = Py_Something(args); ... return pyo; }
使用样例:
static PyObject *subtract_long(long a, long b) { PyObject *pA, *pB, *r; pA = PyLong_FromLong(a); /* pA: New reference. */ pB = PyLong_FromLong(b); /* pB: New reference. */ r = PyNumber_Subtract(pA, pB); /* r: New reference. */ Py_DECREF(pA); /* My responsibility to decref. */ Py_DECREF(pB); /* My responsibility to decref. */ return r; /* Callers responsibility to decref. */ } // 错误的例子,a、b两个PyObject泄漏。 r = PyNumber_Subtract(PyLong_FromLong(a), PyLong_FromLong(b));
Stolen引用
当创建的PyObject传递给其他的容器,例如PyTuple_SetItem、PyList_SetItem。
static PyObject *make_tuple(void) { PyObject *r; PyObject *v; r = PyTuple_New(3); /* New reference. */ v = PyLong_FromLong(1L); /* New reference. */ /* PyTuple_SetItem "steals" the new reference v. */ PyTuple_SetItem(r, 0, v); /* This is fine. */ v = PyLong_FromLong(2L); PyTuple_SetItem(r, 1, v); /* More common pattern. */ PyTuple_SetItem(r, 2, PyUnicode_FromString("three")); return r; /* Callers responsibility to decref. */ }
但是,需要注意PyDict_SetItem内部会引用计数加一。
Borrowed引用
Python文档中,Borrowed引用的体现:
PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos) Return value: Borrowed reference.
Borrowed 引用的所有者不应该调用 Py_DECREF(),使用Borrowed 引用在函数退出时不会出现内存泄露。。但是不要让一个对象处理未保护的状态Borrowed 引用,如果对象处理未保护状态,它随时可能会被销毁。
例如:从一个 list 获取对象,继续操作它,但并不递增它的引用。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将这个对象删除(递减它的引用计数,或者释放它),导致 item 成为一个悬垂指针。
bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); /* BUG! */ } no_bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); Py_INCREF(item); /* Protect item. */ PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); Py_DECREF(item); }
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Object Bot 1.0.0 发布,Java 测试数据管理库
我很高兴地宣布,Object Bot 1.0.0正式发布了。 Object Bot 是什么? Object Bot 是Java 的一个管理测试数据的程序库。 为什么要有 Object Bot? 随着测试数量增多,测试测试数据会散落在各个测试里,每个测试都要编写大量的测试数据初始化相关的代码,而且,通常测试数据还要满足一定的依赖关系,这会让测试数据的初始化代码大幅度增加。 Object Bot 就是为了解决这种困难而产生的。 添加依赖 在项目中添加相应的依赖,如果你的项目用的是 Maven,可以这样添加: <dependency> <groupId>com.github.dreamhead</groupId> <artifactId>bot-junit5</artifactId> <version>1.0.0</version> </dependency> 如果是 Gradle,可以这样添加: dependencies { testImplementation( "com....
- 下一篇
作为DBA,你不得不掌握的压测工具
mysqlslap mysqlslap是MySQL自带的一个用于实现负载性能测试和压力测试的工具。它可以模拟多个客户端对数据库进行施压,并生成报告来了解数据库的性能状况。 mysqlslap的运行过程主要分三步: 创建库、表,导入数据用于测试。此过程由单线程完成。 开始进行压力测试。该步骤可以使用多线程完成。 清理测试数据。此过程由单线程完成。 下面举几个例子来说明下如何使用mysqlslap。 1. 自动生成测试表,其中会生成自增列,采用单线程进行测试。 [root@node1 ~]# mysqlslap -uroot -p --auto-generate-sql --auto-generate-sql-load-type=mixed --auto-generate-sql-add-autoincrementEnter password:Benchmark#运行所有语句的平局时间,单位秒Average number of seconds to run all queries: 0.018 seconds#运行所有语句的最小秒数Minimum number of seconds to ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- 设置Eclipse缩进为4个空格,增强代码规范
- Mario游戏-低调大师作品
- MySQL8.0.19开启GTID主从同步CentOS8
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池