之所以出现循环引用,主要原因是多个容器之间互相引用,致使一些对象虽然没有外部引用,但是 ref_cnt 却不为 0,基于引用计数的内存回收策略失效。如何找到那些循环引用呢?借用 《Python 源码剖析》 中陈儒先生的说法,如果将那些容器之间的引用排除掉,由外部变量发起的引用称为 “有效引用”,下面是两种不同的引用情况:
- Container1、Container2 只有彼此之间的互相引用,所以它们的有效引用实际上为 0;
- Container3 有一个外部变量引用它,它的有效引用是 1,Container4 的有效引用同样为 0;
这些拥有有效引用的变量就是那些还在使用中的变量,并且为了保证有效变量的完整性,从这些变量开始所有直接、简介医用的变量也同样是使用中的变量,不能被释放。这些变量是所有 “可达” 对象引用的出发点,即 root 节点。
为了找到这些关键的 root 节点,Python 的策略是将所有变量之间的引用消除,剩余引用计数依然大于 0 的对象就是 root 节点了。当然,所有对象的 ref_cnt 是非常重要的数据,决定了对象的生死存亡,乱改生死簿肯定是不行的,所以查找 root 节点时使用的实际上是 ref_cnt 的副本: gc_refs,gc_refs 数据没有以单独结构体成员表示,而是复用到 GC_HEAD 中的 gc_prev 指针(低 2 位用作标识位)。基本思路是通过容器的 traverse 方法遍历容器的所有成员,将成员引用对象的引用计数减掉(Modules/gcmodule.c:1054)。还是前面的两种情况为例,消除互相引用后的结果如下:
显然,Container3 是一个 root 节点,同样的,利用 Container3 的 traverse 方法,会使得 Container4 也成为了 “可达” 对象,这一过程会发生在 root 节点的所有直接和简介引用对象上。需要注意的一点,在遍历 reachable 对象时,有可能会遇到一些在较早时期被标记为 unreachable 的对象,它们会被重新 “请回” reachable 链表,毕竟之前没人直到这些对象与 root 节点有这样隐秘的关系。
经过这一系列操作,进行内存回收的 “代” (含比它更年轻的代)对象链表被拆分成两部分:
- reachable
- unreachable
其中 reachable 是那些经过 GC 检验存活的对象,它们会晋升到更老的一代(如果存在更老代的话),unreachable 链表中的对象大多数会被被回收。比较特殊的是那些实现了 finalizer 方法的 unreachable 对象,它们不会被 GC 主动回收,也会晋升到更老一代。
至此,一次 GC 的主要动作就完成了。明天我会绘制一副动图,展示 GC 不同代之间的演变关系。