Python的垃圾回收机制

垃圾回收机制

Python中的垃圾回收是以引用计数为主,分代收集为辅。

引用计数

原理

python中一切皆为对象,对象的核心就是一个结构体PyObject,里面包含引用计数器ob_refcnt,当对象增加一个引用时,ob_refcnt+1;当引用它的对象被删除时,ob_refcnt-1;当引用计数ob_refcnt==0时,对象的生命结束。

1
2
3
4
 typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;

导致引用计数+1的情况
对象被创建,例如a=23
对象被引用,例如b=a
对象被作为参数,传入到一个函数中,例如func(a)
对象作为一个元素,存储在容器中,例如list1=[a,a]
导致引用计数-1的情况
对象的别名被显式销毁,例如del a
对象的别名被赋予新的对象,例如a=24
一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
对象所在的容器被销毁,或从容器中删除对象

1
2
查看一个对象的引用计数
sys.getrefcount(a)可以查看a对象的引用计数,但是比正常计数大1,因为调用函数的时候传入a,这会让a的引用计数+1

优点

简单
实时性:当某个对象的引用计数为0时,内存马上就会被回收;不像其他需要等待特定的时机来进行垃圾回收。这样也带来了另一个好处:垃圾回收的时间也分摊到了平时。

缺点

维护引用计数,消耗资源
致命的缺陷: 循环引用

循环引用

例:

1
2
3
4
5
#循环引用的例子
list1 = []
list2 = []
list1.append(list2) #list1引用list2
list2.append(list1) #list2引用list1

list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。

对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。

标记-清楚算法

面向会出现循环引用的数据类型,eg:list、dict、class等,而int、String等不是。
寻找Root objects(根节点):
使用计数器副本,去除循环引用环后,引用计数不为0的节点就是根节点。
从root object(一般指全局引用或函数栈上的引用)出发,通过引用可以链接到的为可达对象,放入根节点链表;不能链接到的为不可达unreacherable对象,放入不可达节点链表,上面这个过程称为垃圾收集,后面垃圾回收是对不可达链表进行回收。

原理

1、标记-清除机制

标记-清除机制,顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收)。

首先初始所有对象标记为白色,并确定根节点对象(这些对象是不会被删除),标记它们为黑色(表示对象有效)。将有效对象引用的对象标记为灰色(表示对象可达,但它们所引用的对象还没检查),检查完灰色对象引用的对象后,将灰色标记为黑色。重复直到不存在灰色节点为止。最后白色结点都是需要清除的对象。

2、回收对象的组织

这里所采用的高级机制作为引用计数的辅助机制,用于解决产生的循环引用问题。而循环引用只会出现在“内部存在可以对其他对象引用的对象”,比如:list,class等。

为了要将这些回收对象组织起来,需要建立一个链表。自然,每个被收集的对象内就需要多提供一些信息。

通过PyGC_Head的指针将每个回收对象连接起来,形成了一个链表,也就是在1里提到的初始化的所有对象。

分代技术

分代技术是一种典型的以空间换时间的技术,这也正是java里的关键技术。这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。

这样的思想,可以减少标记-清除机制所带来的额外操作。分代就是将回收对象分成数个代,每个代就是一个链表(集合),代进行标记-清除的时间与代内对象存活时间成正比例关系。

基于“对象存活的时间越长,越可能不是垃圾,应该越少进行垃圾收集”的思想。
对象等级共分为0、1、2三代,每代对应一个链表,每代有一个代表各代最多允许的对象数量:threshold(默认情况下,generation 0 超过700,或generation 1、generation2超过10,会触发垃圾回收机制)
所有的新建对象都是0代,当一个对象经历过垃圾回收,依然存活,就会被归入下一代对象
generation 0触发,会将generation 0 、1、2依次链接起来再清理
generation 1触发,会将generation 1、 2依次链接起来再清理
generation 2触发,只会清理自己

GC模块(Garbage Collector interface)

gc模块常用解析

gc模块提供一个接口给开发者设置垃圾回收的选项。上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能就是解决循环引用的问题。

常用函数:

1
gc.set_debug(flags)

设置gc的debug日志,一般设置为gc.DEBUG_LEAK

1
gc.collect([generation])

显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。
返回不可达(unreachable objects)对象的数目

1
gc.set_threshold(threshold0[, threshold1[, threshold2])

设置自动执行垃圾回收的频率。

1
gc.get_count()

获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表

处理流程

这个机制的主要作用就是发现并处理不可达的垃圾对象。
垃圾回收=垃圾检查+垃圾回收
把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾检查中,改对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中。

gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取。
例如(488,3,0),其中488是指距离上一次一代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加。例如:

1
2
3
4
5
6
print gc.get_count() # (590, 8, 0)
a = ClassA()
print gc.get_count() # (591, 8, 0)
del a
print gc.get_count() # (590, 8, 0)

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组,例如(700,10,10)
每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器
当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查一代对象的垃圾,并重置计数器为(0,4,0)
当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查一、二代对象的垃圾,并重置计数器为(0,0,1)
当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)

其他

如果循环引用中,两个对象都定义了__del__方法,gc模块不会销毁这些不可达对象,因为gc模块不知道应该先调用哪个对象的__del__方法,所以为了安全起见,gc模块会把对象放到gc.garbage中,但是不会销毁对象。