09月17, 2020

32. GC 是怎样工作的?

虽然已经再三提到这一点,但是还是需要注意,GC 只管理那些容器对象,比如 List、Dict、Set 之类,这些对象的创建都是通过 _PyObject_GC_Alloc 实现,GC 则在每次通过这个函数创建之时,试图进行内存回收。当然并不是每次都会触发,内存回收需要满足一定的条件,其中最主要的一条是:

// Modules/gcmodule.c:1971

state->generations[0].count > state->generations[0].threshold

0 代对象总数需要超过阈值: 700,所以内存回收的首要条件是 0 代内存的情况。

在继续深入之前,我们先大致对分代回收有一个大致了解。分代思想是基于这样一个假设:

大多数对象都是短暂出现,很快被释放;生存了一段时间的对象,似乎有可能生存更久。

分代思想的目的是将探测的注意力集中在 “临时” 对象上,对于那些内存中 “似乎” 更有可能万年长青的 “长者” 网开一面,容后处理。Python 采用了 3 代模式:

image.png

每经过一次回收,年轻代中逃过 GC 生存下来对象,会被提升到更老的代,享受一些优待,减少对它们的扫描。

现在我们继续,当 0 代对象数量超过阈值 700 后,就会进入 collect_generations 函数:

// Modules/gcmodule.c:1246

static Py_ssize_t
collect_generations(struct _gc_runtime_state *state)
{
    // 忽略一些代码

    // 从最古老代开始检查
    for (int i = NUM_GENERATIONS-1; i >= 0; i--) {
        // 检查阈值
        if (state->generations[i].count > state->generations[i].threshold) {
            // 省略一些代码

            // 对 i 代展开回收
            n = collect_with_callback(state, i);
            break;
        }
    }
    return n;
}

这些代码说明,开始内存回收的条件是由 0 代决定,而具体回收哪几代对象则以最老的代为准。注意这里检测到符合条件的最老一代就立刻 break 了,不要轻易认为只处理这一代,稍后会看到为什么不能这么武断。

从 collect_with_callback 函数开始,短暂的经过 collect_with_callback 函数,很快就进入了最核心的 collect 函数,这里先大致梳理 collect 函数的梗概:

static Py_ssize_t
collect(struct _gc_runtime_state *state, int generation,
        Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable, int nofail)
{
    // ...

    // 将比当前代更年轻代对象移动到当前代对象链表之后,这就是 collect_generations 中直接 break 的原因
    for (i = 0; i < generation; i++) {
        gc_list_merge(GEN_HEAD(state, i), GEN_HEAD(state, generation));
    }

    // GC_HEAD 中的 gc_prev 被用作 gc_refs,将 Object 的 ref_cnt 复制到 gc_prev 中
    update_refs(young);  // gc_prev is used for gc_refs

    // 减去循环引用,此时 gc_refs 大于 0 的对象就是有外部引用的对象,称为 root 对象
    // 与这些 root 对象有直接、间接引用的对象都是 reachable 的
    subtract_refs(young);

    // 初始化一个 unreachable 链表,
    gc_list_init(&unreachable);
    // 将 young 中 unreachable 对象移到 unreachable 链表中,并且在这里重建 gc_prev 指针
    move_unreachable(young, &unreachable); 

    // ...

    // young 链表中残留的是那些存活下来的对象,它们会被提升到更老的代,进入 “养老院” 享受优待。
    gc_list_merge(young, old);

    // ...
    // 一些准备工作

    // 真正回收资源
    finalize_garbage(&unreachable);

    // ...
    // 一些善后工作
}

这样就比较清晰了,检测到某一代内存符合回收条件,所有比其更年轻的对象都会被纳入回收范围。既然每次都是从最古老的对象开始检查,为什么最开始又偏偏以 0 代为开始 gc 的条件呢? 其实很容易理解,除了 0 代,其他代对象都是从 0 代升格的,如果 0 代对象数量没有发生变化,1、2 代的对象数量自然也不会有什么太大变动的。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_32.html

-- EOF --