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

引用计数

在Python中,使用了引用计数这一技术实现内存管理。一个对象被创建完成后就有一个变量指向这个对象,那么就这个对象的引用计数为1,以后如果有其他变量指向这个对象,其引用计数也会相应增加,如果将一个变量不再执行这个对象,那么这个对象的引用计数减1。如果一个对象没有任何变量指向这个对象,也即引用计数为0,那么这个对象会被Python回收。

具体地,引用计数法的原理是每个对象维护一个ob_ref,用来记录当前对象被引用的次数,也就是来追踪到底有多少引用指向了这个对象,当发生以下四种情况的时候,该对象的引用计数器+1:

  1. 对象被创建,比如a=14
  2. 对象被引用,比如b=a
  3. 对象被作为参数,传到函数中,比如func(a)
  4. 对象作为一个元素,存储在容器中,比如List={a,”a”,”b”,2}

与上述情况相对应,当发生以下四种情况时,该对象的引用计数器-1:

  • 当该对象的别名被显式销毁时,比如del a
  • 当该对象的引别名被赋予新的对象,比如a=26
  • 一个对象离开它的作用域,例如func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
  • 将该元素从容器中删除时,或者容器被销毁时。

当指向该对象的内存的引用计数器为0的时候,该内存将会被Python虚拟机销毁

引用计数示例代码如下:

  1. class Person(object):
  2. def __init__(self,name):
  3. self.name = name
  4. def __del__(self):
  5. print('%s执行了del函数'%self.name)
  6. while True:
  7. p1 = Person('p1')
  8. p2 = Person('p2')
  9. del p1
  10. del p2
  11. a = input('test:')

Python里面每一个东西都是对象,他们的核心是一个结构体Py_Object,所有Python对象的头部包含了这样一个结构PyObject

  1. // object.h
  2. struct _object {
  3. Py_ssize_t ob_refcnt; # 引用计数值
  4. struct PyTypeObject *ob_type;
  5. } PyObject;

看一个比较具体点的例子,int型对象的定义:

  1. // intobject.h
  2. typedef struct {
  3. PyObject_HEAD
  4. long ob_ival;
  5. } PyIntObject;

简而言之,PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少。当引用计数为0时,该对象生命就结束了。

引用计数法有很明显的优点

  • 高效。
  • 运行期没有停顿。可以类比一下Ruby的垃圾回收机制,也就是实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
  • 对象有确定的生命周期。
  • 易于实现。

原始的引用计数法也有明显的缺点:

  • 维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比。
  • 无法解决循环引用的问题。比如现在有两个对象分别为aba指向了bb又指向了a,那么他们两的引用计数永远都不会为0。也即永远得不到回收。

循环引用的示例:

  1. class Person(object):
  2. def __init__(self, name):
  3. self.name = name
  4. def __del__(self):
  5. print('%s执行了del函数' % self.name)
  6. while True:
  7. p1 = Person('p1')
  8. p2 = Person('p2')
  9. p1.next = p2
  10. p2.prev = p1
  11. del p1
  12. del p2
  13. a = input('test:')

为了解决这两个致命弱点,Python又引入了以下两种GC机制。

标记清除

针对循环引用的情况:我们有一组未使用的、互相指向的对象,但是谁都没有外部引用。换句话说,我们的程序不再使用这些节点对象了,所以我们希望Python的垃圾回收机制能足够智能去释放这些对象并回收它们占用的内存空间。但是这不可能,因为所有的引用计数都是1而不是0。Python的引用计数算法不能够处理互相指向自己的对象。你的代码也许会在不经意间包含循环引用并且你并未意识到。事实上,当你的Python程序运行的时候它将会建立一定数量的“浮点数垃圾”,Python的GC不能够处理未使用的对象因为应用计数值不会到零。这就是为什么Python要引入Generational GC算法的原因!


标记清除算法是一种基于追踪回收技术实现的垃圾回收算法。它分为两个阶段:

  • 第一阶段是标记阶段,GC会把所有的活动对象打上标记
  • 第二阶段是把那些没有标记的对象非活动对象进行回收。

那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。见下图:
Python中的垃圾回收机制 - 图1
在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如listdicttupleinstance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

正如Ruby使用一个链表(free list)来持续追踪未使用的、自由的对象一样,Python使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python的内部C代码将其称为零代(Generation Zero)。在Python程序中,每次新创建了一个对象,那么就会将这个对象挂到一个叫做零代链表中。如果创建的对象总和减去被释放的对象,达到一定的值(某个阈值),那么Python就会遍历这个零代链表,找到那些有相互引用的对象,将这些对象的引用计数减1,如果引用计数值为0了,那么就说明这个对象是可以被释放的。接下来再将没有被释放的对象,挪动到一个新的链表中,这个链表叫做一代链表

在零代链表清理的次数达到某个阈值后,Python会去遍历一代链表,将那些没有得到释放的对象移动到二代链表。同样的原理,如果一代链表清理的次数达到某个阈值后,Python会去遍历二代链表,把垃圾对象进行回收。

Python中的 GC 阈值

从某种意义上说,Python的GC算法类似于Ruby所用的标记回收算法。周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的,这正类似于Ruby的标记过程。

Python什么时候会进行这个标记过程?随着程序地运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。

但是事实并非如此。因为循环引用的原因,并且因为程序种中使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。

随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。

通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。

弱代假说

来看看分代垃圾回收算法的核心行为:垃圾回收器会更频繁的处理新对象。一个新的对象即是你的程序刚刚创建的,而一个来的对象则是经过了几个时间周期之后仍然存在的对象。Python会在当一个对象从零代移动到一代,或是从一代移动到二代的过程中提升这个对象。

这种算法的根源来自于弱代假说(weak generational hypothesis)。这个假说由两个观点构成:

  • 新的对象通常死得更快;
  • 老对象很有可能存活更长的时间。

假定现在我用Python或是Ruby创建一个新对象 n1=”ABC”,根据假说,我的代码很可能仅仅会使用ABC很短的时间。这个对象也许仅仅只是一个方法中的中间结果,并且随着方法的返回这个对象就将变成垃圾了。大部分的新对象都是如此般地很快变成垃圾。然而,偶尔程序会创建一些很重要的,存活时间比较长的对象,例如web应用中的session变量或是配置项。

通过频繁的处理零代链表中的新对象,Python的垃圾收集器将把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足阈值的条件,收集器才回去处理那些老变量。

分代回收

先给出gc的逻辑:首先分配内存,一旦发现超过阈值了,则触发垃圾回收,然后将所有可收集对象链表放到一起进行遍历,计算有效引用计数,将其分成【有效引用计数=0】和【有效引用计数 > 0】两个集合,将大于0的,放入到更老一代,等于0的,执行回收。回收遍历容器内的各个元素,减掉对应元素引用计数(破掉循环引用),执行-1的逻辑,若发现对象引用计数等于0,触发内存回收,python底层内存管理机制回收内存。

Python中引入了分代收集,总共三个”代”。在Python 中,一个代就是一个链表,所有属于同一”代”的内存块都链接在同一个链表中。

Python默认定义了三代对象集合,索引数越大,对象存活时间越长。新生成的对象会被加入第0代,每新生成一个对象都会检查第0代有没有满,如果满了就开始着手进行垃圾回收。

简单来说,分代回收是一种以空间换时间的操作方式。

Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。

参考