在使用 cpython 时, 发现偶尔会发生内存泄露。这是什么原因呢?
从python内存管理机制开始说起默认的内存分配器
python 中所有内存管理机制都有两套实现,通过编译符号 PYMALLOC_DEBUG 控制,在debug模式下可以记录很多关于内存的信息,方便开发时进行调试。
python内存管理机制
python内存管理机制大致被分为四层
操作系统提供的内存管理接口,比如malloc 和 free 接口,由操作系统实现和管理Python对于第0层的一个简单包装,主要是为了统一不同操作系统的行为,以PyMem_为前缀python 创建对象时不仅仅需要申请一块内存空间,还需要管理对象的类型参数以及初始化对象的引用计数值。以PyObj_为前缀。最上层针对一些常用的字符串对象和整数对象,提供一个对象缓存的机制。而在Python中,对象间的引用是通过引用计数来管理的。当一个对象被引用时,其引用计数会增加;当不再被引用时,其引用计数会减少。当引用计数为0时,该对象就会被垃圾回收器回收。
PyTuple_SetItem 源码分析
intPyTuple_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem){ PyObject **p; if (!PyTuple_Check(op) || op->ob_refcnt != 1) { Py_XDECREF(newitem); PyErr_BadInternalCall(); return -1; } if (i < 0 || i >= Py_SIZE(op)) { Py_XDECREF(newitem); PyErr_SetString(PyExc_IndexError, "tuple assignment index out of range"); return -1; } p = ((PyTupleObject *)op) -> ob_item + i; Py_XSETREF(*p, newitem); return 0;}首先 python 中 元组应该是不可变的,因此只有当其引用计数为1,也就是没有其他人调用这个元组时,我们才可以对其元素进行更改。 因此最开始先要确认是否是元组类型以及元祖的引用计数是否为1,如果不满足要求,则会让newitem的引用计数减少1。
注意:如果在此之前 newitem 的引用计数为1 ,经过减少则会变成0,那么这个时候就相当于此函数会“偷走”一个对 newitem 的引用。可能会导致该元素提前被释放. 这可能会造成内存风险,不过不是泄露。
接下来会进行索引有效性检查,风险同上,这里不过多赘述。
接下来会进行赋值,这个过程中,旧的元素的引用计数会减少(如果变成0,也会提前被释放,不过这里是符合预期的),新的元素的计数会增加。
总结也就是说PyTuple_SetItem 存在两种可能,第一种:新元素引用计数减1,第二种旧元素引用计数减1,新元素引用计数加1
因此我们必须要关注PyTuple_SetItem 的操作是否会成功,如果成功,我们需要手动释放之前创建的new_item(更准确的说,是减引用计数)。 如果失败,我们就不能对new_item有任何操作。
作者:山花链接:https://juejin.cn/post/7366972839780089906