Python源码到内存管理机制(笔记版)

介绍Python的内存管理机制,先从 Python 的对象布局讲起,以最简单的浮点数为例,在 Include/floatobject.h 中,python 中的浮点数是这么定义的:

typedef struct {
    
    
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

我们继续查看 PyObject_HEAD 的定义:

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   \
    _PyObject_HEAD_EXTRA                \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

这是一个宏定义,而其中的 EXTRA 在正常编译时是空的。所以,我们直接展开所有宏,那么 PyFloatObject 的定义就是这样子的:

typedef struct {
    
    
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
    double ob_fval;
} PyFloatObject;

这样就很清楚了,ob_refcnt 就是引用计数,而 ob_fval 是真正的值。例如我写一段这样的代码:

a = 1000.0
a = 2000.0

在执行第 1 句时,Python 虚拟机真正执行的逻辑是创建一个 PyFloatObject 对象,然后使它的 ob_fval 为 1000.0,同时,它的引用计数为 1;当执行到第 2 句时,创建一个值为 2000.0 的 PyFloatObject 对象,并且使这个对象的引用计数为 1,而前一个对象的引用计数就要减 1,从而变成 0。那么前一个对象就会被回收。

在 Python 中,引用计数的维护是通过这两个宏来实现的:

#define Py_INCREF(op) (                         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
    ((PyObject*)(op))->ob_refcnt++)
#define Py_DECREF(op)                                   \
    do {
    
                                                    \
        if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
        --((PyObject*)(op))->ob_refcnt != 0)            \
            _Py_CHECK_REFCNT(op)                        \
        else                                            \
        _Py_Dealloc((PyObject *)(op));                  \
    } while (0)

这两个宏位于 Include/object.h 中。这段代码里最重要的地方在于 ob_refcnt 增一和减一的操作。

使用了引用计数的地方,就会存在循环引用。例如下图中的四个对象,A 是根对象,它与 B 之间有循环引用,那么它们都不是垃圾对象。C 和 D 之间也有循环引用,但因为没有外界的引用指向它们了,所以它们就是垃圾对象,但是循环引用导致他们都不能释放。
在这里插入图片描述

Python 为了解决这个问题,在虚拟机中引入了一个双向链表,把所有对象都放到这个链表里。Python 的每个对象头上都有一个名为 PyGC_Head 的结构:

/* GC information is stored BEFORE the object structure. */
typedef union _gc_head {
    
    
    struct {
    
    
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
} PyGC_Head;

在这个结构里,gc_next 和 gc_prev 的作用就是把对象关联到链表里。而 gc_refs 则是用于消除循环引用的。当链表中的对象达到一定数目时,Python 的 GC 模块就会执行一次标记清除。具体来讲,一共有四步。

第一步,将 ob_refcnt 的值复制到 gc_refs 中。对于上面的例子,它们的 gc_refs 的值就如下图所示:
在这里插入图片描述

第二步是遍历整个链表,对每个对象,将它直接引用的对象的 gc_refs 的值减一。比如遍历到 A 对象时,只把 B 对象的 gc_refs 值减一;遍历到 B 对象时,再把它直接引用的 A 对象的 gc_refs 值减一。经过这一步骤后,四个对象的 gc_refs 的值如下图所示:
在这里插入图片描述

第三步,将 gc_refs 值为 0 的对象,从对象链表中摘下来,放入一个名为“临时不可达”的链表中。之所以使用“临时”,是因为有循环引用的垃圾对象的 gc_refs 在此时一定为 0,比如 C 和 D。但 gc_refs 值为 0 的对象不一定是垃圾对象,比如 B 对象。此时,B、C 和 D 对象就被放入临时不可达链表中了,示意图如下所示:
在这里插入图片描述

最后一步,以可达对象链表中的对象为根开始深度优先搜索,将所有访问到 gc_refs 为 0 的对象,再从临时不可达链表中移回可达链表中。最后留在临时不可达链表中的对象,就是真正的垃圾对象了。

接下来就可以使用 _Py_Dealloc 逐个释放链表中的对象了,对于上面的例子,就是把 B 对象重新加回到可达对象链表中,然后将 C 和 D 分别释放。

扩展资料

猜你喜欢

转载自blog.csdn.net/weixin_43849871/article/details/129554866