虽然已经再三提到这一点,但是还是需要注意,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 代模式:
每经过一次回收,年轻代中逃过 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 代的对象数量自然也不会有什么太大变动的。