楔子

Python 的垃圾回收是通过标记-清除和分代收集实现的,下面就通过源码来考察一下。

我们知道,清理一代链表时会顺带清理零代链表,总之就是把比自己"代"小的链子也清理了。那么这是怎么做到的呢?其实答案就在 gc_list_merge 函数中。

如果清理的是一代链表,那么在开始垃圾回收之前,Python 会将零代链表(比它年轻的),整个链接到一代链表之后,这样的话在清理一代的时候也会清理零代。当然啦,清理二代链表也是同理,会将一代链表和零代链表,整个链接到二代链表之后,这样清理二代的时候也会清理一代和零代。

// Include/cpython/objimpl.h

// 获取 PyGC_Head 的 _gc_next 字段,它保存了可收集对象链表中下一个 container 对象的地址
#define _PyGCHead_NEXT(g)        ((PyGC_Head*)(g)->_gc_next)

// 获取 PyGC_Head 的 _gc_prev 字段,它保存了可收集对象链表中上一个 container 对象的地址
// 但由于 _gc_prev 的后两位(低地址的两位)有其它用,所以要和 _PyGC_PREV_MASK 按位与
#define _PyGCHead_PREV(g) ((PyGC_Head*)((g)->_gc_prev & _PyGC_PREV_MASK))

// _gc_prev 的倒数第一个位表示 tp_finalize 是否被调用
#define _PyGC_PREV_MASK_FINALIZED  (1)
// _gc_prev 的倒数第二个位表示对象所处的"代"是否正在被 GC
#define _PyGC_PREV_MASK_COLLECTING (2)
// _gc_prev 的前 62 个位表示地址,由于 uintptr_t 是 64 位无符号整型
// 所以 (((uintptr_t) -1) << _PyGC_PREV_SHIFT) 会得到一个前 62 个位是 1,后 2 个位是 0 的数
// 因此 _gc_prev & _PyGC_PREV_MASK 得到的就是地址
#define _PyGC_PREV_SHIFT           (2)
#define _PyGC_PREV_MASK            (((uintptr_t) -1) << _PyGC_PREV_SHIFT)

// 将 g->_gc_next 设置为 p
#define _PyGCHead_SET_NEXT(g, p) ((g)->_gc_next = (uintptr_t)(p))

// 将 g->_gc_prev 设置为 p,注意:前 62 个位表示地址
#define _PyGCHead_PREV(g) ((PyGC_Head*)((g)->_gc_prev & _PyGC_PREV_MASK))


// Modules/gcmodule.c
static void
gc_list_merge(PyGC_Head *from, PyGC_Head *to)
{
    // from 和 to 分别表示对应的链表的虚拟头结点(dummyHead)
    assert(from != to);
    if (!gc_list_is_empty(from)) {
        // 因为是双向链表,所以 to 的 _prev 是对应链表的尾节点
        PyGC_Head *to_tail = GC_PREV(to);
        // from 的 _gc_next 是对应链表的真实头结点
        PyGC_Head *from_head = GC_NEXT(from);
        // from 的 _gc_prev 是对应链表的尾节点
        PyGC_Head *from_tail = GC_PREV(from);
        assert(from_head != from);
        assert(from_tail != from);
        // 更新指针
        _PyGCHead_SET_NEXT(to_tail, from_head);
        _PyGCHead_SET_PREV(from_head, to_tail);

        _PyGCHead_SET_NEXT(from_tail, to);
        _PyGCHead_SET_PREV(to, from_tail);
    }
    gc_list_init(from);
}

gc_list_merge 做的事情就是将 from 链表合并到 to 链表的末尾,假设清理的是零代链表,那么这里的 from 就是零代链表,to 就是一代链表,此后的标记-清除算法就在 merge 之后的那一条链表上进行。

在探究标记-清除垃圾回收方法之前,我们需要建立一个简单的循环引用的例子。

lst1 = []
lst2 = []

lst1.append(lst2)
lst2.append(lst1)

# 注意这里多了一个外部引用
a = lst1

lst3 = []
lst4 = []

lst3.append(lst4)
lst4.append(lst3)

示意图如下:

数字指的是当前对象的引用计数,显然 lst1 为 3,其它的都为 2,因为有一个额外的变量 a 也指向了 lst1 指向的对象。而 lst1 和 lst2,lst3 和 lst4 之间均发生了循环引用。

寻找 root object 集合

按照之前对垃圾收集算法的一般性描述,如果要使用标记-清除算法,首先需要找到 root object。那么在上面的那幅图中,哪些是属于 root object 呢?

让我们换个角度来思考,前面提到 root object 是不能被删除的对象。也就是说,在可收集对象链表的外部存在着对该对象的引用,删除这个对象会导致错误的行为,而在当前这个例子中显然只有 lst1 属于 root object。但这仅仅是观察的结果,那么如何设计一种算法来得到这个结果呢?

我们注意到这样一个事实,如果两个对象的引用计数都为 1,但仅仅是它们之间存在着循环引用,那么这两个对象是需要被回收的。也就是说,尽管它们的引用计数表现为非 0,但实际上有效的引用计数为 0。

这里提出了一个有效引用计数的概念,为了获得有效的引用计数,必须将循环引用的影响消除,或者将这个闭环从引用中摘除(循环引用在有向图中会形成一个环),而具体的实现方式就是将两个对象的引用计数都减去 1。这样一来,两个对象的引用计数都会变成 0,我们便挥去了循环引用的迷雾,使有效引用计数现出了真身。

那么如何使两个对象的引用计数都减 1 呢,很简单,假设这两个对象为 A 和 B,那么从 A 出发,由于它有一个对 B 的引用,于是将 B 的引用计数减 1;然后顺着引用达到 B,发现它有一个对 A 的引用,那么同样会将 A 的引用计数减1,这样就完成了循环引用对象间环的删除。

总结一下就是,Python 会寻找那些具有循环引用的对象,并尝试把它们的引用计数都减去 1。

但是这样就出现了一个问题,假设可收集对象链表中的 container 对象 A 有一个对对象 C 的引用,而 C 并不在这个链表中。如果在 A 没有被回收的情况下,将 C 的引用计数减 1,那么显然 C 的引用计数会被错误地减少 1,这将导致未来的某个时刻对 C 的引用会出现悬空。要想解决这个问题,就需要我们在 A 没有被删除的情况下恢复 C 的引用计数,可如果采用这样的方案的话,那么维护引用计数的复杂度将成倍增长。

换一个角度,其实有更好的做法,我们不改动真实的引用计数,而是改动引用计数的副本。对于副本,无论做什么样的改动,都不会影响对象生命周期的维护,因为它唯一的作用就是寻找 root object 集合。那么这个副本在哪里体现呢?答案是 PyGC_Head 的 _gc_prev 字段。这里有人可能好奇了,_gc_prev 不是用来存储地址的吗。

  • 在正常时期:_gc_prev 的前 62 个位用来存储地址;
  • 在 GC 时期:_gc_prev 的前 62 个位用来存储引用计数副本。

所以这是两个不同时期的用途,我们看一下源码。

// Include/cpython/objimpl.h

#define _PyGC_PREV_MASK_FINALIZED  (1)
#define _PyGC_PREV_MASK_COLLECTING (2)
#define _PyGC_PREV_SHIFT           (2)
#define _PyGC_PREV_MASK            (((uintptr_t) -1) << _PyGC_PREV_SHIFT)

// Modules/gcmodule.c

#define PREV_MASK_COLLECTING   _PyGC_PREV_MASK_COLLECTING

static inline int
gc_is_collecting(PyGC_Head *g)
{
    // 对象所处的代是否正在被 GC,如果 _gc_prev 的倒数第二位是 1,表示正在被 GC
    return (g->_gc_prev & PREV_MASK_COLLECTING) != 0;
}

static inline void
gc_clear_collecting(PyGC_Head *g)
{
    // 将 _gc_prev 的倒数第二位设置为 0,表示对象所处的代没有被 GC
    g->_gc_prev &= ~PREV_MASK_COLLECTING;
}

static inline Py_ssize_t
gc_get_refs(PyGC_Head *g)
{
    // 将 _gc_prev 右移 2 位,返回引用计数副本
    return (Py_ssize_t)(g->_gc_prev >> _PyGC_PREV_SHIFT);
}

static inline void
gc_set_refs(PyGC_Head *g, Py_ssize_t refs)
{   
    // 让 _gc_prev 的前 62 个位存储引用计数副本
    g->_gc_prev = (g->_gc_prev & ~_PyGC_PREV_MASK)
        | ((uintptr_t)(refs) << _PyGC_PREV_SHIFT);
}

static inline void
gc_reset_refs(PyGC_Head *g, Py_ssize_t refs)
{
    // 只保留 _gc_prev 的倒数第一位,然后将倒数第二位设置为 1
    // 再让前 62 个位保存引用计数副本
    g->_gc_prev = (g->_gc_prev & _PyGC_PREV_MASK_FINALIZED)
        | PREV_MASK_COLLECTING
        | ((uintptr_t)(refs) << _PyGC_PREV_SHIFT);
}

在垃圾回收的第一步,就是遍历可收集对象链表,将每个对象的引用计数拷贝给引用计数副本。

// Modules/gcmodule.c
static void
update_refs(PyGC_Head *containers)
{
    // 获取链表的真实头结点
    PyGC_Head *gc = GC_NEXT(containers);
    for (; gc != containers; gc = GC_NEXT(gc)) {
        // FROM_GC(gc) 会拿到 PyObject 的地址,再调用 Py_REFCNT 便可拿到引用计数
        // 然后将引用计数拷贝给 PyGC_Head 的 _gc_prev 字段,作为副本
        gc_reset_refs(gc, Py_REFCNT(FROM_GC(gc)));
        _PyObject_ASSERT(FROM_GC(gc), gc_get_refs(gc) != 0);
    }
}

而接下来的动作就是要将环引用摘除。

// Modules/gcmodule.c
static void
subtract_refs(PyGC_Head *containers)
{
    traverseproc traverse;
    PyGC_Head *gc = GC_NEXT(containers);
    // 遍历链表的每一个对象
    for (; gc != containers; gc = GC_NEXT(gc)) {
        // 获取 PyObject 的地址
        PyObject *op = FROM_GC(gc);
        // 调用类型对象的 tp_traverse 字段
        traverse = Py_TYPE(op)->tp_traverse;
        (void) traverse(FROM_GC(gc),
                       (visitproc)visit_decref,
                       op);
    }
}

我们注意到里面有一个 tp_traverse,介绍类型对象的时候说过这个字段,它用于垃圾回收的检测阶段,通过遍历对象所引用的其它对象,确定对象之间的引用关系,帮助垃圾回收器识别出所有活跃的对象和出现循环引用的对象。除了 tp_traverse 之外还有一个和它搭配的 tp_clear,用于清除阶段,负责减少出现循环引用的对象的引用计数。

因为 tp_traverse 定义在类型对象中,所以它和特定的 container 对象有关。一般来说,tp_traverse 的动作就是遍历 container 对象中的每一个引用,然后对引用执行 visit_decref 操作。

// Include/object.h
typedef int (*visitproc)(PyObject *, void *);
typedef int (*traverseproc)(PyObject *, visitproc, void *);

// Include/objimpl.h
#define Py_VISIT(op)                                                    \
    do {                                                                \
        if (op) {                                                       \
            int vret = visit(_PyObject_CAST(op), arg);                  \
            if (vret)                                                   \
                return vret;                                            \
        }                                                               \
    } while (0)

// Objects/listobject.c
PyTypeObject PyList_Type = {
    //...
    (traverseproc)list_traverse,     /* tp_traverse */
    //...
};

static int
list_traverse(PyListObject *o, visitproc visit, void *arg)
{
    Py_ssize_t i;
    // 对列表中的每一个元素都执行 Py_VISIT,Py_VISIT 是一个宏,会调用参数 visit
    // 而 visit 就是 subtract_refs 函数内部在调用 tp_traverse 时传递的 visit_decref 函数
    for (i = Py_SIZE(o); --i >= 0; )
        Py_VISIT(o->ob_item[i]);
    return 0;
}

// Modules/gcmodule.c
static int
visit_decref(PyObject *op, void *parent)
{
    _PyObject_ASSERT(_PyObject_CAST(parent), !_PyObject_IsFreed(op));
    // 如果 op 指向的对象参与垃圾回收
    if (PyObject_IS_GC(op)) {
        // 基于 PyObject 的地址获取 PyGC_Head 的地址
        PyGC_Head *gc = AS_GC(op);
        // 如果所处的代正在被 GC
        if (gc_is_collecting(gc)) {
            // 调用 gc_decref 减少引用计数
            gc_decref(gc);
        }
    }
    return 0;
}

比如我们要删除一个列表,那么显然在删除之前,列表里面每个元素的引用计数肯定要减一。

在完成了 subtract_refs 之后,可收集对象链表中所有 container 对象之间的环引用就被摘除了。这时有一些 container 对象的 PyGC_Head._gc_prev 还不为 0,这就意味着存在对这些对象的外部引用,这些对象就是开始标记-清除算法的 root object。

举个栗子:

import sys

lst1 = []
lst2 = []

lst1.append(lst2)
lst2.append(lst1)

# 注意这里多了一个外部引用
a = lst1

print(sys.getrefcount(a))  # 4
print(sys.getrefcount(lst1))  # 4
print(sys.getrefcount(lst2[0]))  # 4

由于 sys.getrefcount 函数本身会多一个引用,所以减去 1 的话,结果都是 3,表示它们指向的对象的引用计数为 3。所以这个时候 a 就想到了,除了我,还有两位老铁 lst1 和 lst2[0] 也指向了我指向的对象。

垃圾标记

还是以上面的代码为例。

import sys

lst1 = []
lst2 = []

lst1.append(lst2)
lst2.append(lst1)

# 注意这里多了一个外部引用
a = lst1

print(sys.getrefcount(a))  # 4
print(sys.getrefcount(lst1))  # 4
print(sys.getrefcount(lst2[0]))  # 4

假设现在执行了删除操作 del lst1, lst2, lst3, lst4,那么成功地寻找到 root object 集合之后,我们就可以从 root object 出发,沿着引用链,一个接一个地标记不能回收的内存。但是从 root object 出发前,需要先将现在的内存链表一分为二,一条链表维护 root object 集合,成为 root 链表;而另一条链表中维护剩下的对象,成为 unreachable 链表。

由于 unreachable 链表上面的对象都是可回收的垃圾,那么显然目前的 unreachable 链表是名不副实的,或者说不符合上面的对象都可回收的条件。因为里面可能存在被 root 链表中的对象直接引用或间接引用的对象,这些对象是不可以回收的,因此一旦在标记中发现了这样的对象,那么就应该将其从 unreachable 移到 root 链表中。当完成标记之后,unreachable 链表中剩下的对象就是名副其实的垃圾对象了,那么接下来的垃圾回收只需要限制在 unreachable 链表中即可。

正如我们一开始介绍的三色标记模型,确定完 root object 集合之后,就假设剩下的对象都是不可达的。然后遍历,如果可达,证明我们冤枉它了,那么就为它平冤昭雪(标记为可达)。为此 Python 专门准备了一条名为 unreachable 的链表,通过 move_unreachable 函数完成了对原始链表的切分。

// Modules/gcmodule.c
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    // 记录 young 链表中的前一个节点
    PyGC_Head *prev = young;
    // 从 young 链表的真实头结点开始遍历
    PyGC_Head *gc = GC_NEXT(young);

    // 遍历整个 young 链表,直到 gc 等于链表的 dummyHead
    while (gc != young) {
        // 如果引用计数不为 0,说明对象可达
        if (gc_get_refs(gc)) {
            // 获取 PyObject 的地址
            PyObject *op = FROM_GC(gc);
            // 获取 tp_traverse
            traverseproc traverse = Py_TYPE(op)->tp_traverse;
            _PyObject_ASSERT_WITH_MSG(op, gc_get_refs(gc) > 0,
                                      "refcount is too small");
            // 通过 tp_traverse 调用 visit_reachable 标记可达对象
            (void) traverse(op,
                    (visitproc)visit_reachable,
                    (void *)young);
            // 重新链接 prev 指针
            _PyGCHead_SET_PREV(gc, prev);
            // 清除 collecting 标记,即对象不再处于 GC 中
            gc_clear_collecting(gc);
            prev = gc;
        }
        // 引用计数为 0
        else {
            // 对象可能不可达,将其移到 unreachable 链表
            // 首先从 young 链表中移除
            prev->_gc_next = gc->_gc_next;
            // 加入 unreachable 链表的尾部
            PyGC_Head *last = GC_PREV(unreachable);
            last->_gc_next = (NEXT_MASK_UNREACHABLE | (uintptr_t)gc);
            _PyGCHead_SET_PREV(gc, last);
            gc->_gc_next = (NEXT_MASK_UNREACHABLE | (uintptr_t)unreachable);
            unreachable->_gc_prev = (uintptr_t)gc;
        }
        gc = (PyGC_Head*)prev->_gc_next;
    }
    // 更新 young 链表尾指针
    young->_gc_prev = (uintptr_t)prev;
}

函数调用完之后会将 young 链表中的对象进行分类,young 链表保留可达对象,unreachable 链表存放不可达对象。

然后是里面的 visit_reachable 函数,它是怎么将对象标记为可达的呢?来看一下。

// Modules/gcmodule.c
static int
visit_reachable(PyObject *op, PyGC_Head *reachable)
{
    // 如果对象不参与垃圾回收,直接返回
    if (!PyObject_IS_GC(op)) {
        return 0;
    }
    // 获取 PyGC_Head 的地址
    PyGC_Head *gc = AS_GC(op);
    // 获取引用计数
    const Py_ssize_t gc_refs = gc_get_refs(gc);
    // 忽略未跟踪对象和其它代的对象
    if (gc->_gc_next == 0 || !gc_is_collecting(gc)) {
        return 0;
    }
    // 对象虽然在 unreachable 链表中,但实际是可达的,那么从 unreachable 链表中移除
    if (gc->_gc_next & NEXT_MASK_UNREACHABLE) {
        PyGC_Head *prev = GC_PREV(gc);
        PyGC_Head *next = (PyGC_Head*)(gc->_gc_next & ~NEXT_MASK_UNREACHABLE);
        _PyObject_ASSERT(FROM_GC(prev),
                         prev->_gc_next & NEXT_MASK_UNREACHABLE);
        _PyObject_ASSERT(FROM_GC(next),
                         next->_gc_next & NEXT_MASK_UNREACHABLE);
        prev->_gc_next = gc->_gc_next;  // copy NEXT_MASK_UNREACHABLE
        _PyGCHead_SET_PREV(next, prev);

        gc_list_append(gc, reachable);
        gc_set_refs(gc, 1);
    }
    else if (gc_refs == 0) {
        // 对象在 young 链表中,但还未被遍历到,标记为可达
        gc_set_refs(gc, 1);
    }
    // 无需处理
    else {
        _PyObject_ASSERT_WITH_MSG(op, gc_refs > 0, "refcount is too small");
    }
    return 0;
}

所以该函数主要处理三种情况:

  • unreachable 中的可达对象:移回 young 链表。
  • young 中未遍历的对象:标记为可达。
  • 其它可达对象:无需处理。

当 move_unreachable 完成之后,最初的一条链表就被切分成了两条链表,在 unreachable 链表中,就是发现的垃圾对象,是垃圾回收的目标。

但是等一等,在 unreachable 链表中,所有的对象都可以安全回收吗?垃圾回收在清理对象的时候,一旦发现对象的类对象里面定义了 __del__ 函数,那么在清理该对象的时候就会调用 __del__,因此也叫析构函数,这是 Python 为开发人员提供的在对象被销毁时进行某些资源释放的 Hook 机制。

现在问题来了,我们已经知道最终在 unreachable 链表中出现的对象都是只存在循环引用的对象,需要被销毁。但是假如现在在 unreachbale 中,有两个对象 A 和 B, 对象 B 在 __del__ 中调用了对象 A 的某个操作,这意味着安全的垃圾回收必须保证对象 A 要在对象 B 之后被回收,但 Python 无法做到这一点,Python 在回收垃圾时不能保证回收的顺序。因此有可能 A 已经被销毁了,然后 B 在销毁时又访问已经不存在的 A,毫无疑问,Python 遇到麻烦了。虽然同时满足存在 __del__ 和循环引用这两个条件的概率非常低,但 Python 不能对此置之不理。

于是 Python 采用了一种保守的做法,会将 unreachable 链表中拥有 __del__ 的实例对象统统都移到 _gc_runtime_state.garbage 中。

// Include/internal/pycore_pymem.h
struct _gc_runtime_state {
    // 需要延迟删除的对象组成的单向链表,通过 gc_prev 指针连接
    PyObject *trash_delete_later;
    // tp_dealloc 的递归调用深度,防止递归过深
    int trash_delete_nesting;
    // 是否启用 GC
    int enabled;
    // 是否启用调试模式
    int debug;
    // 存储三条可收集对象链表的虚拟头结点
    struct gc_generation generations[NUM_GENERATIONS];
    // 零代链表的虚拟头结点,为优化访问效率,单独使用一个字段保存
    PyGC_Head *generation0;
    // 永久代,存放不需要回收的对象
    struct gc_generation permanent_generation;
    // 记录每代的统计信息(收集次数、对象数等)
    struct gc_generation_stats generation_stats[NUM_GENERATIONS];
    // 标记当前是否正在进行 GC,防止 GC 重入
    int collecting;
    // 指向一个列表,里面存储了产生循环引用但无法安全回收的对象(一般是重写了 __del__)
    PyObject *garbage;
    // GC 事件回调函数列表,允许外部监控 GC 过程
    PyObject *callbacks;
    // 完整 GC(第二代)后存活的对象数,用于评估长期存活的对象数量
    Py_ssize_t long_lived_total;
    // 在非完整 GC 中存活且等待首次完整 GC 的对象数,用于优化 GC 策略
    Py_ssize_t long_lived_pending;
};

这些字段共同构成了 Python GC 的运行时状态,支持分代回收、延迟删除、调试等功能。

垃圾回收

要回收 unreachable 链表中的垃圾对象,就必须先打破对象间的循环引用,上面我们已经阐述了如何打破循环引用的办法,下面来看看具体的销毁过程。

// Modules/gcmodule.c

// 检查链表是否为空(链表中是否只有 dummyHead)
static inline int
gc_list_is_empty(PyGC_Head *list)
{
    // 对于一个双向链表来说,如果为空
    // 那么它的 _gc_next、_gc_prev 都指向 dummyHead 自身
    return (list->_gc_next == (uintptr_t)list);
}

// 将对象添加到链表的尾节点
static inline void
gc_list_append(PyGC_Head *node, PyGC_Head *list)
{
    // list 就是 dummyHead,它的上一个节点显然是链表的尾节点
    PyGC_Head *last = (PyGC_Head *)list->_gc_prev;
    // node->_gc_prev = last
    _PyGCHead_SET_PREV(node, last);
    // last->_gc_next = node
    _PyGCHead_SET_NEXT(last, node);
    // node->_gc_next = dummyHead
    _PyGCHead_SET_NEXT(node, list);
    // dummyHead->_gc_prev = node
    list->_gc_prev = (uintptr_t)node;
}

// 将对象从链表中摘除
static inline void
gc_list_remove(PyGC_Head *node)
{
    // node 的上一个节点
    PyGC_Head *prev = GC_PREV(node);
    // node 的下一个节点
    PyGC_Head *next = GC_NEXT(node);
    // 让 node 的上一个节点的 _gc_next 指向 node 的下一个节点
    _PyGCHead_SET_NEXT(prev, next);
    // 让 node 的下一个节点的 _gc_prev 指向 node 的上一个节点
    _PyGCHead_SET_PREV(next, prev);
    // 此时便完成了对象的摘除
    // 将 _gc_next 设置为 0,表示不再被 GC 跟踪
    node->_gc_next = 0; /* object is not currently tracked */
}

static void
delete_garbage(struct _gc_runtime_state *state,
               PyGC_Head *collectable, PyGC_Head *old)
{
    assert(!PyErr_Occurred());
    // 遍历 collectable 链表直到为空
    while (!gc_list_is_empty(collectable)) {
        // 获取可收集对象链表中的下一个 container 对象,但拿到的是 PyGC_Head 的地址
        PyGC_Head *gc = GC_NEXT(collectable);
        // 基于 PyGC_Head 的地址获取 PyObject 的地址
        PyObject *op = FROM_GC(gc);
        // 断言引用计数 > 0
        _PyObject_ASSERT_WITH_MSG(op, Py_REFCNT(op) > 0,
                                  "refcount is too small");
        // debug 模式:保存到 garbage 列表
        if (state->debug & DEBUG_SAVEALL) {
            assert(state->garbage != NULL);
            if (PyList_Append(state->garbage, op) < 0) {
                PyErr_Clear();
            }
        }
        // 非 debug 模式:尝试清理对象
        else {
            inquiry clear;
            // 调用 tp_clear
            if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
                Py_INCREF(op);
                (void) clear(op);
                if (PyErr_Occurred()) {
                    _PyErr_WriteUnraisableMsg("in tp_clear of",
                                              (PyObject*)Py_TYPE(op));
                }
                Py_DECREF(op);
            }
        }
        // 如果对象仍在原位置,说明还活着,移到 old 链表,但它后续可能会死亡(稍后解释相关细节)
        if (GC_NEXT(collectable) == gc) {
            gc_list_move(gc, old);
        }
    }
}

整个过程会调用 container 对象的类型对象中的 tp_clear 字段,该调用会调整 container 对象中引用的其它对象的引用计数,从而打破循环引用的最终目标。还是以 PyListObject 为例:

// Objects/listobject.c
PyTypeObject PyList_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "list",
    sizeof(PyListObject),
    // ...
    (traverseproc)list_traverse,                /* tp_traverse */
    (inquiry)_list_clear,                       /* tp_clear */
    // ...
};

static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        // 获取 ob_size
        i = Py_SIZE(a);
        // 因为要被销毁了,所以将 ob_size 设置为 0
        Py_SIZE(a) = 0;
        // 将指向指针数组的二级指针设置为 NULL
        a->ob_item = NULL;
        // 容量设置为 0
        a->allocated = 0;
        // 数组里面指针指向的对象,也全部减少引用计数
        // 因为列表要被销毁了,不再持有对它们的引用
        while (--i >= 0) {
            Py_XDECREF(item[i]);
        }
        // 释放数组所占的内存
        PyMem_FREE(item);
    }
    return 0;
}

我们注意到,在 delete_garbage 中,有一些 unreachable 链表中的对象会被重新送回到 reachable 链表(即 delete_garbage 的 old 参数)中。这是由于进行 clear 动作时,如果成功进行,通常一个对象会把自己从可收集对象链表中摘除。但由于某些原因,对象可能在 clear 动作时,没有成功完成必要的动作,从而没有将自己从 collectable 链表中摘除。这表示对象认为自己还不能被销毁,所以 Python 需要将这种对象放回到 reachable 链表中。

然后当对象被销毁时,肯定要调用析构函数,我们在上面看到了_list_clear。假设是调用了 lst3 的 _list_clear,那么不好意思,接下来会调用 lst4 的析构函数。因为 lst3 和 lst4 存在循环引用,所以调用了 lst3 的 _list_clear 会减少 lst4 的引用计数。由于这两位老铁都被删除了,还惺惺相惜赖在内存里面不走,所以将 lst4 的引用计数减少 1 之后,只能归于湮灭了,会调用其 list_dealloc,注意:这时候调用的是 lst4 的 list_dealloc。

// Objects/listobject.c
static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    // 从可收集对象链表中移除
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_BEGIN(op, list_dealloc)
    if (op->ob_item != NULL) {
        i = Py_SIZE(op);
        // 依次遍历,减少内部元素的引用计数
        while (--i >= 0) {
            Py_XDECREF(op->ob_item[i]);
        }
        // 释放内存
        PyMem_FREE(op->ob_item);
    }
    // 缓存池机制
    if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
        free_list[numfree++] = op;
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
    Py_TRASHCAN_END
}

调用 lst3 的 _list_clear,减少内部元素引用计数的时候,会导致 lst4 的引用计数为 0。而一旦 lst4 的引用计数为 0,那么是不是也要执行和 lst3 一样的 _list_clear动作呢?然后会发现 lst3 的引用计数也为 0 了,因此 lst3 也会被销毁,准确的说是指向的对象被销毁。

循环引用,彼此共生,销毁之路,怎能独自前行?最终 lst3 和 lst4 都会执行内部的 list_dealloc,释放内部元素,调整参数,当然还有所谓的缓存池机制等等。总之如此一来,lst3 和 lst4 指向的对象就都被安全地回收了。

虽然有很多对象挂在可收集对象链表中,但大部分时间都是引用计数机制在维护这些对象,只有面对引用计数无能为力的循环引用,垃圾收集机制才会起到作用。这里没有把引用计数看成垃圾回收机制的一种,事实上,如果不是循环引用的话,那么垃圾回收完全不用出马。因为没有循环引用的话,垃圾回收的作用相当于只是将对象标记为可达,并移入下一代链表。

所以挂在可收集对象链表上的对象都是引用计数不为 0 的,如果为 0 早被引用计数机制干掉了。而引用计数不为 0 的情况也有两种:一种是被程序使用的对象,二是产生循环引用的对象。被程序使用的对象是不能被回收的,所以垃圾回收只能处理那些循环引用的对象。

这里多提一句,可收集对象链表中的对象越多,那么垃圾回收发动一次的开销就越大。假设有一个类的实例对象,显然它也是需要被 GC 跟踪的,但如果我们能保证这个对象一定不会发生循环引用,那么可不可以不让它参与 GC 呢?因为不会发生循环引用,GC 检测也只是在做无用功,这样还不如不检测。

答案是可以的,当我们写 C 扩展的时候可以这么做,但是纯 Python 代码不行,解释器没有在 Python 的层面将这一特性暴露出来。因为解释器并不知道实际会不会产生循环引用,所以只要是有能力产生循环引用的 container 对象,统统会被 GC 跟踪,也就是会被挂在可收集对象链表上。而我们在用 C 或 Cython 写扩展时是可以实现的,如果能够人为保证某个对象一定不会出现循环引用,那么可以不让它参与 GC,从而降低 GC 的成本。

PyObject_GC_Track

container 对象如果想成为可收集对象,那么必须调用 PyObject_GC_Track 函数加入到可收集对象链表中,这个函数之前看到过,但没有详细介绍,这里再补充一下。

// Modules/gcmodule.c
void
PyObject_GC_Track(void *op_raw)
{
    // 将对象加入到可收集对象链表中,也就是让对象被 GC 跟踪
    PyObject *op = _PyObject_CAST(op_raw);
    // 如果对象已经被 GC 跟踪了(_gc_next 不为 0),那么报错
    if (_PyObject_GC_IS_TRACKED(op)) {
        _PyObject_ASSERT_FAILED_MSG(op,
                                    "object already tracked "
                                    "by the garbage collector");
    }
    // 调用 _PyObject_GC_TRACK
    _PyObject_GC_TRACK(op);
}

// Include/internal/pycore_object.h
#define _PyObject_GC_TRACK(op) \
    _PyObject_GC_TRACK_impl(__FILE__, __LINE__, _PyObject_CAST(op))

static inline void _PyObject_GC_TRACK_impl(const char *filename, int lineno,
                                           PyObject *op)
{
    // 如果对象已经被 GC 跟踪,那么报错
    _PyObject_ASSERT_FROM(op, !_PyObject_GC_IS_TRACKED(op),
                          "object already tracked by the garbage collector",
                          filename, lineno, "_PyObject_GC_TRACK");
    // _Py_AS_GC 和之前介绍的 AS_GC 做的事情是一样的,都是基于 PyObject 的地址得到 PyGC_Head 的地址
    PyGC_Head *gc = _Py_AS_GC(op);
    // _gc_prev 的倒数第二个位表示对象所处的"代"是否正在被 GC
    // 如果它和 _PyGC_PREV_MASK_COLLECTING 按位与之后不等于 0,说明正在被 GC,那么报错
    // 注意:这里的第二个参数(断言条件)有点反直觉,刚好是相反的
    _PyObject_ASSERT_FROM(op,
                          (gc->_gc_prev & _PyGC_PREV_MASK_COLLECTING) == 0,
                          "object is in generation which is garbage collected",
                          filename, lineno, "_PyObject_GC_TRACK");
    // _PyRuntime.gc.generation0 表示零代链表的头结点,准确来说是虚拟头结点(dummyHead)
    // 那么它的 _gc_prev 字段便是零代链表中的最后一个对象的地址
    PyGC_Head *last = (PyGC_Head*)(_PyRuntime.gc.generation0->_gc_prev);
    // 将 last->_gc_next 设置为 gc
    _PyGCHead_SET_NEXT(last, gc);
    // 将 gc->_gc_prev 设置为 last
    _PyGCHead_SET_PREV(gc, last);
    // 将 gc->_gc_next 设置为 _PyRuntime.gc.generation0,即链表的虚拟头结点
    _PyGCHead_SET_NEXT(gc, _PyRuntime.gc.generation0);
    // 将虚拟头结点的 _gc_prev 设置为 gc
    _PyRuntime.gc.generation0->_gc_prev = (uintptr_t)gc;
    
    // 以上几步执行完之后,新的对象就插入到了零代链表的尾部
}

所以这个函数的作用就是将对象插入到零代链表的尾部,非常简单,这里算是补充了一下之前遗漏的内容。当然除了 PyObject_GC_Track 之外,还有 PyObject_GC_UnTrack,可以自己看一下。

对象的弱引用

再来聊一聊弱引用,关于引用分为两种,分别是强引用和弱引用。默认情况下,引用都是强引用,会导致对象的引用计数加 1,而弱引用则不会导致对象的引用计数增加。

如何实现弱引用

如果想实现弱引用,需要使用 weakref 模块,一般来说这个模块用的比较少,因为弱引用本身用的就不多。但是弱引用在很多场景中,可以发挥出很神奇的功能。

import weakref

class RefObject:

    def __del__(self):
        print("del executed")


obj = RefObject()
# 对象的弱引用通过 weakref.ref 类来创建
r = weakref.ref(obj)
print(obj) 
"""
<__main__.RefObject object at 0x7faf3a1578b0>
"""
# 显示关联 RefObject
print(r)
"""
<weakref at 0x7faf3a293d60; to 'RefObject' at 0x7faf3a1578b0>
"""

# 对引用进行调用的话, 即可得到原对象
print(r() is obj)  
"""
True
"""

# 删除 obj 会执行析构函数
del obj  
"""
del executed
"""

# 之前说过 r() 等价于 obj, 但是 obj 被删除了, 所以返回 None
# 从这里返回 None 也能看出这个弱引用是不会增加引用计数的
print("r():", r()) 
"""
r(): None
"""
# 打印弱引用, 告诉我们状态已经变成了 dead
print(r)  
"""
<weakref at 0x7faf3a293d60; dead>
"""

通过弱引用可以实现缓存的效果,当弱引用的对象存在时,则对象可用;当对象不存在时,则返回 None,程序不会因此而报错。这个和缓存本质上是一样的,也是一个有则用、无则重新获取的技术。

此外 weak.ref 还可以接收一个可选的回调函数,删除引用指向的对象时就会调用这个回调函数。

import weakref

class RefObject:

    def __del__(self):
        print("del executed")


obj = RefObject()
r = weakref.ref(obj, lambda ref: print("引用被删除了", ref))
del obj  
print("r():", r()) 
"""
del executed
引用被删除了 <weakref at 0x7faf3a0deae0; dead>
r(): None
"""
# 回调函数会接收一个参数, 也就是死亡之后的弱引用

前面说了,对象的弱引用会由单独的字段保存,也就是保存在列表中。当对象被删除时,会遍历这个列表,依次执行弱引用绑定的回调函数。

创建弱引用除了通过 weakref.ref 之外,还可以使用代理。有时候使用代理比使用弱引用更方便,使用代理可以像使用原对象一样,而且不要求在访问对象之前先调用代理。这说明,可以将代理传递到一个库,而这个库并不知道它接收的是一个代理,而不是一个真正的对象。

import weakref

class RefObject:

    def __init__(self, name):
        self.name = name

    def __del__(self):
        print("del executed")


obj = RefObject("my obj")
r = weakref.ref(obj)
p = weakref.proxy(obj)

# 可以看到引用加上()才相当于原来的对象
# 而代理不需要,直接和原来的对象保持一致
print(obj.name)  # my obj
print(r().name)  # my obj
print(p.name)  # my obj

# 但是注意: 弱引用在调用之后就是原对象, 而代理不是
print(r() is obj)  # True
print(p is obj)  # False

del obj  # del executed

try:
    # 删除对象之后, 再调用引用, 打印为 None
    print(r())  # None
    # 如果是使用代理, 则会报错
    print(p)
except Exception as e:
    print(e)  # weakly-referenced object no longer exists

weakref.proxy 和 weakref.ref 一样,也可以接收一个额外的回调函数。

字典的弱引用

weakref 专门提供了 key 为弱引用或 value 为弱引用的字典,先来看看普通字典。

class A:

    def __del__(self):
        print("__del__")

a = A()

# 创建一个普通字典
d = {}
# 由于 a 作为了字典的 key, 那么 a 指向的对象的引用计数会加 1, 变成 2
d[a] = "xxx"

# 删除 a, 对对象无影响, 不会触发析构函数
del a
print(d)
"""
{<__main__.A object at 0x7f27bdeb8c70>: 'xxx'}
"""
# 删除字典,内部元素的引用计数减 1,因此会触发对象的析构
del d
"""
__del__
"""

但如果是对 key 为弱引用的字典的话,就不一样了。

import weakref

class A:

    def __del__(self):
        print("__del__")

a = A()

# 创建一个弱引用字典, 它的 api 和普通字典一样
d = weakref.WeakKeyDictionary()
print("d:", d)  
"""
d: <WeakKeyDictionary at 0x7fc1c529f370>
"""

# 此时 a 指向的对象的引用计数不会增加
d[a] = "xxx"
print("before del a:", list(d.items()))
"""
before del a: [(<__main__.A object at 0x7fc1c52a2c70>, 'xxx')]
"""

# 删除 a, 对象会被回收
del a
"""
__del__
"""
print("after del a:", list(d.items()))
"""
after del a: []
"""

key 为弱引用的字典不会增加 key 的引用计数,并且当对象被回收时,会自动从字典中消失。当然除了可以创建 key 为弱引用的字典,还可以创建 value 为弱引用的字典。

import weakref

class A:

    def __del__(self):
        print("__del__")

a = A()

d = weakref.WeakValueDictionary()
# value 为弱引用
d["xxx"] = a
print("before del a:", list(d.items()))
"""
before del a: [('xxx', <__main__.A object at 0x7fd20737dc70>)]
"""
# 删除 a, 对象会被回收
del a
"""
__del__
"""
print("after del a:", list(d.items()))
"""
after del a: []
"""

整个过程是一样的,当对象被回收时,键值对会自动从字典中消失。除了字典,我们还可以创建弱引用集合,将对象放入集合中不会增加对象的引用计数。

import weakref

class A:

    def __del__(self):
        print("__del__")

a = A()

s = weakref.WeakSet()
s.add(a)
print(len(s))
del a
print(len(s))
"""
1
__del__
0
"""

让自定义类支持弱引用

每一个自定义类的实例,都会有自己的属性字典 __dict__。而我们知道字典使用的是哈希表,这是一个空间换时间的数据结构,因此如果想省内存的话,那么通常的做法是指定 __slots__ 属性,这样实例就不会再有属性字典 __dict__ 了。

import weakref

class A:

    __slots__ = ("name", "age")

    def __init__(self):
        self.name = "古明地觉"
        self.age = 17

a = A()

try:
    weakref.ref(a)
except Exception as e:
    print(e)  # cannot create weak reference to 'A' object

try:
    weakref.proxy(a)
except Exception as e:
    print(e)  # cannot create weak reference to 'A' object

try:
    d = weakref.WeakSet()
    d.add(a)
except Exception as e:
    print(e)  # cannot create weak reference to 'A' object

此时我们发现,A 的实例对象没办法被弱引用,因为指定了 __slots__。那么要怎么解决呢?很简单,直接在 __slots__ 里面加一个属性就好了。

import weakref

class A:

    # 多指定一个 __weakref__, 表示支持弱引用
    __slots__ = ("name", "age", "__weakref__")

    def __init__(self):
        self.name = "古明地觉"
        self.age = 17

a = A()

weakref.ref(a)
weakref.proxy(a)
d = weakref.WeakSet()
d.add(a)

没有报错,可以看到此时就支持弱引用了。

从 C 的角度来看强引用和弱引用

首先 C 源代码变成可执行文件会经历如下几个步骤:

  • 预处理:进行头文件展开,宏替换等等;
  • 编译:通过词法分析和语法分析,将预处理之后的文件翻译成汇编代码,内存分配也是在此过程完成的;
  • 汇编:将汇编代码翻译成目标文件,目标文件中存放的也就是和源文件等效的机器代码;
  • 链接:程序中会引入一些外部库,需要将目标文件中的符号与外部库的符号链接起来,最终形成一个可执行文件;

而在链接这一步,这些符号必须能够被正确决议,如果没有找到某些符号的定义,连接器就会报错,这种就是强引用。而对于弱引用,如果该符号有定义,则链接器将该符号的引用决议,如果该符号未被定义,则链接器也不会报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误的值。一般对于未定义的弱引用,链接器默认其为 0,或者是一个其它的特殊的值,以便于程序代码能够识别。

弱引用是怎么实现的

弱引用看起来很神奇,但实现起来比想象中简单的多。对象本质上是一个结构体实例,结构体内部会有一个字段专门负责维护该对象的弱引用。

从注释可以看出这个字段指向一个列表,而弱引用在创建之后,会添加到该列表中。由于弱引用保存了原对象的指针和一个回调函数(但是不增加对象的引用计数),所以当原对象被销毁时,虚拟机会遍历这个列表,清理弱引用保存的指针并执行回调函数(如果设置了)。

但是奇怪了,我们之前在介绍整数、浮点数等结构的时候,没有看到类似 weakreflist 这样的字段啊。是的,所以它们无法被弱引用。

Python 的 gc 模块

这个 gc 模块之前提到过,它是用 C 编写的,源码对应 Modules/gcmodule.c,当 Python 编译好时,就内嵌在解释器里面了。我们可以导入它,但在 Python 安装目录里面是看不到的。

gc.enable():开启垃圾回收

这个函数表示开启垃圾回收机制,默认是自动开启的。

gc.disable():关闭垃圾回收

import gc

class A:
    pass

# 关掉 gc
gc.disable()

while True:
    a1 = A()
    a2 = A()

    # 此时内部出现了循环引用
    a1.__dict__["attr"] = a2
    a2.__dict__["attr"] = a1

    # 由于循环引用,所以光靠引用计数是不够的
    # 还需要垃圾回收,但是我们给关闭了
    del a1, a2

看一下内存占用:

无限循环,并且每次循环都会创建新的对象,最终导致内存占用无限增大,几秒钟的时间便增长到 33 个 G。

import gc

class A:
    pass

# 关掉 gc
gc.disable()

while True:
    a1 = A()
    a2 = A()

这里我们依旧关闭了 GC,但由于每一次循环都会指向一个新的对象,而之前的对象由于没有变量指向了,那么引用计数为 0,直接就被引用计数机制干掉了,内存会一直稳定,不会出现增长。

所以即使关闭了 GC,对于那些引用计数为 0 的,该删除还是会删除的。因此引用计数很简单,就是按照对应的规则该加 1 加 1,该减 1 减 1,一旦为 0,直接销毁。而当出现循环引用的时候,才需要 GC 闪亮登场。因此这里虽然关闭了 GC,但没有循环引用,所以没事。

而第一个例子出现了循环引用,而引用计数机制只会根据引用计数来判断,发现引用计数不为 0,所以就一直傻傻地不回收,程序又一直创建新的对象,最终导致内存越用越多。如果第一个例子开启了 GC,那么就会通过标记-清除的方式将产生循环引用的对象的引用计数减 1,而引用计数机制发现引用计数为 0 了,就会将对象回收掉。

gc.isenabled():判断 GC 是否开启

import gc

print(gc.isenabled())  # True
gc.disable()
print(gc.isenabled())  # False

默认是开启的。

gc.collect():立刻触发垃圾回收

import gc

class A:

    def __init__(self, name):
        self.name = name

    def __del__(self):
        print(f"{self.name} 被删除了")

a1 = A("古明地觉")
a2 = A("古明地恋")

# 发生循环引用
a1.obj = a2
a2.obj = a1

# 无事发生
del a1, a2
print("------")

# 强行触发垃圾回收,参数表示指定的"代"
# 目前对象显然是在零代链表上,应该清理零代
# 但是清理一代和二代也可以,因为会顺带清理零代
gc.collect(0)
"""
------
古明地觉 被删除了
古明地恋 被删除了
"""

gc.get_threshold():返回每一代的阈值

import gc

print(gc.get_threshold())  # (700, 10, 10)

gc.set_threshold():设置每一代的阈值

import gc

gc.set_threshold(1000, 100, 100)
print(gc.get_threshold())  # (1000, 100, 100)

gc.get_count():查看每一代的值达到了多少

import gc

# 你的结果可能和我这里不一样
print(gc.get_count())  # (133, 7, 8)

gc.get_stats():返回每一代的具体信息

from pprint import pprint
import gc

pprint(gc.get_stats())
"""
[{'collected': 678, 'collections': 95, 'uncollectable': 0},
 {'collected': 569, 'collections': 8, 'uncollectable': 0},
 {'collected': 0, 'collections': 0, 'uncollectable': 0}]
"""

gc.get_objects():返回一个列表,里面是被垃圾回收器跟踪的所有对象

import gc

print(gc.get_objects())

打印的内容会很多,因为有大量的 container 对象在被跟踪。

gc.is_tracked(obj):查看对象 obj 是否被垃圾回收器跟踪

import gc

a = 1
b = []

print(gc.is_tracked(a))  # False
print(gc.is_tracked(b))  # True

# 只有那些有能力产生循环引用的对象才会被垃圾回收器跟踪

gc.get_referrers(obj):返回所有引用了 obj 的对象

gc.get_referents(obj):返回所有被 obj 引用了的对象

import gc

lst = [[1, 2, 3]]

# 引用了 lst[0] 的对象,显然就是 lst 本身
# 因为引用的对象可以有多个,所以会返回一个列表,因此结果是 [lst]
print(gc.get_referrers(lst[0]))  # [[[1, 2, 3]]]

# 被 lst[0] 引用的对象,显然是三个整数
print(gc.get_referents(lst[0]))  # [3, 2, 1]

gc.freeze():冻结所有被垃圾回收器跟踪的对象,并在以后的垃圾回收中不处理

gc.unfreeze():取消所有冻结的对象,让它们继续参与垃圾回收

gc.get_freeze_count():获取冻结的对象的个数

import gc

# 不需要参数,会自动找到被垃圾回收器跟踪的对象
gc.freeze()
# 说明有很多内置对象在被跟踪,但被我们冻结了
print(gc.get_freeze_count())  # 40132

# 再冻结一个
b = []
gc.freeze()

# 只要打印的结果比上面多 1 就行
print(gc.get_freeze_count())  # 40133

# 取消冻结
gc.unfreeze()
print(gc.get_freeze_count())  # 0

gc.get_debug():获取 debug 级别

gc.set_debug():设置 debug 级别

import gc

print(gc.get_debug())  # 0

# DEBUG_STATS:在垃圾收集过程中打印所有统计信息
# DEBUG_COLLECTABLE:打印发现的可收集对象
# DEBUG_UNCOLLECTABLE:打印 unreachable 对象(除了 uncollectable 对象)
# DEBUG_SAVEALL:将对象保存到 gc.garbage(一个列表)里面,而不是释放它
# DEBUG_LEAK:对内存泄漏的程序进行 debug(everything but STATS)

class A:
    pass

class B:
    pass

a = A()
b = B()

gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_SAVEALL)
print(gc.garbage)  # []

a.b = b
b.a = a
del a, b
gc.collect()  # 强制触发垃圾回收

# 下面都是自动打印的
"""
gc: collecting generation 2...
gc: objects in each generation: 213 3203 36737
gc: objects in permanent generation: 0
gc: done, 4 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 39551
gc: objects in permanent generation: 0
gc: done, 0 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 630 0 39421
gc: objects in permanent generation: 0
gc: done, 19090 unreachable, 0 uncollectable, 0.0150s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 36645
gc: objects in permanent generation: 0
gc: done, 5792 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 36526
gc: objects in permanent generation: 0
gc: done, 373 unreachable, 0 uncollectable, 0.0000s elapsed
"""

print(gc.garbage)
"""
[<__main__.A object at 0x000001BBEEBC3670>, 
 <__main__.B object at 0x000001BBBE145780>, 
 {'b': <__main__.B object at 0x000001BBBE145780>}, 
 {'a': <__main__.A object at 0x000001BBEEBC3670>}]
"""

以上就是 gc 模块相关的内容,对于平常的业务开发来说,使用频率不高。

小结

Python 采用了最经典的(最土的)引用计数机制来作为自动管理内存的方案,但由于无法解决循环引用,于是又引入标记-清除、分代收集,进行了极大的完善。

尽管引用计数机制需要花费额外的开销来维护引用计数,但是在如今这个年代,这点开销算个啥。而且引用计数也有好处,不然早就随着时代的前进而被扫进历史的垃圾堆里面了。至于好处有两点:第一,引用计数机制很方便,很直观,由于大部分对象都不会出现循环引用,所以引用计数机制能够直接解决,不需要什么复杂的操作;第二,引用计数机制将对象回收的开销分摊在了整个运行时,这对 Python 的响应是有好处的。

当然内存管理和垃圾回收是一门非常精细和繁琐的技术,有兴趣的话可以自己大刀阔斧地冲进 Python 的源码中自由翱翔。


 

欢迎大家关注我的公众号:古明地觉的编程教室。

如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。