简介

Python3 的垃圾回收机制(Garbage Collection)分为三点:

  • 引用计数 — Reference Counting
  • 标记-清除 — Mark and Sweep
  • 分代回收 — Generational Collection

引用计数 — Reference Counting

什么是引用计数?

参考下维基百科的定义:

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。

— wikipedia

Python 中一切皆对象,每一个对象的核心是一个结构体:PyObject, 在这个结构体内有一个引用计数器(ob_refcnt

typedef struct_object {
    int ob_refcnt;
    struct_typeobject *ob_type;
} PyObject;

优缺点

  • 优点
    • 尽快的回收不再被使用的对象
    • 回收的过程中不会导致长时间的停顿
    • 可以清晰的标明一个对象的生存周期
  • 缺点
    • 频繁更新引用计数会降低运行效率
    • 无法解决循环引用的问题(两个对象甲乙,甲持有乙并且乙持有甲,此时甲乙的引用计数会永远大于等于1。)

标记-清除 — Mark and Sweep

什么是标记清除?

标记清除:先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成后,恢复运行线程。这样会导致大量零碎的空闲空间碎片,并且使得大容量 对象不容易获得连续的内存空间,而造成空间浪费。

— wikipedia

Python中的细节

为了解决循环引用的问题,Python 引入了该算法,他在垃圾回收时分成了两个阶段:

  • 标记阶段
    • 从根对象开始遍历所有对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象可达。
  • 清除
    • 再次遍历对象,如果发现某个对象没有标记为可达,就将其回收掉。

为了实现上述的两个步骤,Python 维护了两个链表,一个是 root 链表,用于存放待扫描的对象,另一个是 unreachable 链表,用于存放不可达对象。每个对象的 gc_ref 会被初始化为引用计数的值。

具体过程如下:

  1. 遍历 root 链表的对象,将当前对象所引用的对象的 gc_ref减一
  2. 再次扫描 root 链表的对象,如果对象的 gc_ref为0,那么这个对象就被标记为 GC_TENTATIVELY_UNREACHABLE,并且被移动到 Unreachable链表中。
  3. 如果对象的gc_ref不为0,那么这个对象就会被标记为 GC_REACHABLE。当发现有一个节点是可达的时候,会递归的将从该节点出发可以到达的所有节点标记为为 GC_REACHABLE,如果该节点在 Unreachable链表中的话,还需要将其移回到 root 链表中。
  4. 将存在于Unreachable链表中的对象释放掉。

优缺点

  • 优点
    • 解决了引用计数无法解决的循环引用的问题
  • 缺点
    • 过程需要中断程序
    • 产生大量零碎的空闲空间碎片

分代回收 — Generational Collection

什么是分代回收?

分代回收将 程序所拥有的内存空间分成若干分区,并标记为年轻代空间和年老代空间。程序运行所需的对象会先存放在年轻代分区,年轻代分区会较为频繁进行较为激进的垃圾回收行为,每次回收完成后,幸存的存储对象内的寿命计数器加一。当年轻代分区存储对象的寿命计数器达到一定的阈值或存储对象的占用空间超过一定的阈值之后,则被移动到年老代分空间,年老代空间会较少的运行垃圾回收行为。一般情况下,还有永久代的空间,用于涉及程序整个生命周期的对象存储,例如运行代码、数据常量等,该空间通常不进行垃圾回收的操作。通过分代,存货在局限域,小容量,寿命短的存储对象会被快速回收;存活在全局域,大容量,寿命长的存储对象就较少被回收行为处理干扰。

— wikipedia

Python 中的分代回收

在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收的方法,以空间换时间的方法提高垃圾回收效率。

分代回收是基于这样的一个统计事实,对于程序,存在一定比例的内存块的生存周期比较短;而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。生存期较短对象的比例通常在 80% ~ 90% 之间,这种思想简单点说就是:对象存在时间越长,越不可能是垃圾,应该越少去收集。这样在执行标记-清除算法时可以有效减少遍历的对象数,从而提高垃圾回收的速度。

Python gc 给对象定义了三种 generation (zero,one,two),每一个新生对象在 generation zero 中,如果它在一轮 gc 扫描中活了下来,那么它将被移至 generation one,在那里他将较少的被扫描,如果他有活过了一轮 gc,它将被移至 generation two,在那里他被扫描的次数将会更少。

gc的扫描在什么时候触发呢?当某一 generation 中被分配的对象与被释放的对象之差达到某一阈值的时候,就会触发对该 generation 的扫描(同时也会扫描比该 generation 年轻的 generation )。

Python 中,可以同过 gc 模块提供的两个参数进行查看和调整:

import gc

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

set_threshold()中的三个参数threshold0, threshold1, threshold2进行介绍。gc会记录自从上次收集以来新分配的对象数量与释放的对象数量,当两者之差超过threshold0的值时,gc的扫描就会启动,初始的时候只有generation zero被检查。如果自从generation one最近一次被检查以来,generation zero被检查超过threshold1次,那么对generation one的检查将被触发。相同的,如果自从generation two最近一次被检查以来,generation one被检查超过threshold2次,那么对generation two的检查将被触发。get_threshold()是获取三者的值,默认值为(700,10,10)

优缺点

  • 优点
    • 改善 GC 所花费的时间,提高 GC 效率
  • 缺点
    • 额外的空间开销

总结

  • Python 中主要通过 引用计数 进行垃圾回收
  • 通过 标记清除 解决循环引用的问题
  • 通过 分代策略 以空间换时间的方法提高垃圾回收效率

参考链接