引用计数
python的GC主要依靠引用计数来进行垃圾回收。python给所有对象维护一个引用计数的属性,在一个引用被创建或复制的时候,相关对象的引用计数+1,引用被销毁的时候相关对象的引用计数-1。对象引用计数为0的时候,系统会立即回收该内存
// object.h
struct _object {
Py_ssize_t ob_refcnt; // 引用计数值
struct PyTypeObject *ob_type;
}
每个python对象的头部均包含了其引用计数值。
如果只有引用计数,那么当对象出现循环引用的时候,会出现内存泄漏。
import sys
class test():
def __init__(self):
pass
a1 = test()
a2 = test()
a1._self = a1
print sys.getrefcount(a1)
print sys.getrefcount(a2)
这段代码的结果为3 和 2 (给getrefcount方法传参的时候会增加1,对只有一个引用的对象调用该方法时理论上的结果都是2)
标记-清除
『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
标记清除算法作为辅助的GC方法,主要对容器对象进行处理。(字符、数值等类型不会产生循环应用)
对所有对象进行遍历,检测是否要进行垃圾回收时,这个遍历的过程会花费比较多的时间,而实际要进行回收的对象很少。
分代回收
Generation Zero
当一个对象被创建的时候,python会将其加入Generation Zero链表.
容器对象头部的PyGC_Head
// objimpl.h
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy; /* force worst-case alignment */
} PyGC_Head;
GC阈值
因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。
随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。