1.循环引用
循环引用就是对象的引用形成了循环,其计数永不为0。循环引用也有2种情形
2.1多个对象之间相互引用
实例如下:
a = [1, 2]
b = [3, 4]
a.append(b)
b.append(a)
del a
del b
1.2 单个对象引用自身
a = [1, 2]
a.append(a)
2.标记-清除(Mark and Sweep)
python采用"标记-清除"算法来解决容器对象可能产生的循环引用问题。这里要注意,只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义对象等)。像数字、字符串这类简单类型不会出现循环引用。作为一种优化策略,对于只包含简单类型的元组也不在标记清除算法的考虑之列。
该算法在进行垃圾回收时拢共分两步。
步骤1:标记阶段。该阶段会遍历所有的对象,如果是可达的,即还有对象引用它,那么就标记该对象为可达。
步骤2:清除阶段。再次遍历对象,如果发现某个对象没有标记可达,则将其回收。
2.1 标记步骤
对象之间会通过引用连接在一起,构成一个有向图。对象构成有向图的节点,而引用关系构成有向图的边。从root object出发,沿着有向边遍历对象,可达的对象标记为活动对象,不可达的对象就是要被清除的非活动对象。这里解释一下root object,它指的其实就是一些全局变量、调用栈、寄存器,这些对象时不可删除的。
如上图所示,我们把黑圈视为root object。从其出发,对象1可达,那么它将会被标记,对象2和3间接可达也会被标记,而4和5不可达。那么1、2、3就是活动对象,4和5就是非活动对象而被GC回收。
在标记清除算法中,为了追踪容器对象,需要每个容器对象维护两个额外的指针,用来将容器对象组成一个双端链表,指针分别指向前后两个容器对象,便于插入和删除操作。python解释器维护了两个这样的双端链表,一个链表A存放着需要被扫描的容器对象,另一个B存放着临时不可达对象。
对于链表中每一个对象,除了有一个记录记录当前引用计数的变量ref_count,还有一个gc_ref变量。gc_ref变量是ref_count的一个副本。
2.2 清除步骤
gc启动的时候,会逐个遍历A链表中的额容器对象,并且将当前对象的所有引用对象的gc_ref值减1。遍历完成后,解除了循环引用对引用计数的影响。然后,gc会在此扫描所有容器对象。如果对象的gc_ref为0,那么这个对象就被标记为GC_TENTATIVELY_UNREACHABLE,并且被移到链表B中。如果对象的gc_ref不为0,则该对象就会被标记为GC_REACHABLE。除了将所有可达节点标记为GC_REACHABLE之外,如果该节点当前在链表B中,需要将其移到链表A中。这时,存在于链表B中的容器对象就是真正需要被释放的对象。
上述该阶段,会暂停整个应用程序,等待标记清除结束后才会恢复应用程序段的运行。
2.3 优缺点
标记-清除算法的优点在于可以解决循环引用的问题,并且在整个算法执行的过程中没有额外的开销。缺点在于当执行标记清除时正常的程序会阻塞。另外,标记清除在执行多次后,程序的堆空间会产生一些很小的内存碎片。