在使用 cpython 时, 发现偶尔会发生内存泄露。这是什么原因呢?

从python内存管理机制开始说起

默认的内存分配器

python 中所有内存管理机制都有两套实现,通过编译符号 PYMALLOC_DEBUG 控制,在debug模式下可以记录很多关于内存的信息,方便开发时进行调试。

python 指定显示存储最多的gpu python内存设置_内存管理

python内存管理机制

python内存管理机制大致被分为四层

  1. 操作系统提供的内存管理接口,比如malloc 和 free 接口,由操作系统实现和管理
  2. Python对于第0层的一个简单包装,主要是为了统一不同操作系统的行为,以PyMem_为前缀
  3. python 创建对象时不仅仅需要申请一块内存空间,还需要管理对象的类型参数以及初始化对象的引用计数值。以PyObj_为前缀。
  4. 最上层针对一些常用的字符串对象和整数对象,提供一个对象缓存的机制。

而在Python中,对象间的引用是通过引用计数来管理的。当一个对象被引用时,其引用计数会增加;当不再被引用时,其引用计数会减少。当引用计数为0时,该对象就会被垃圾回收器回收。

PyTuple_SetItem 源码分析

int
PyTuple_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有任何操作。

作者:山花