Python 垃圾回收机制

Python 垃圾回收机制学习。

  Python 的垃圾回收采用的是引用计数机制为主,标记-清除分代收集 两种机制为辅的策略。

引用计数

  Python 中万物皆对象,它们的核心就是一个结构体:PyObject

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

  其中 ob_refcnt 就是做为引用计数。当一个对象有新的引用时,它的 ob_refcnt 就会增加,当引用它的对象被删除,它的 ob_refcnt 就会减少。因此,由于 Python 中的每个对象都有一个引用计数,用来计数该对象在不同场所分别被引用了多少次。每当引用一次 Python 对象,相应的引用计数就增1,每当消毁一次 Python 对象,则相应的引用就减 1,只有当引用计数为零时,才真正从内存中删除 Python 对象。

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

  引用计数机制的优点

  1. 简单;
  2. 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

  引用计数机制的缺点

  1. 维护引用计数消耗资源;
  2. 循环引用。

  什么是循环引用?A 和 B 相互引用而再没有外部引用 A 与 B 中的任何一个,它们的引用计数虽然都为1,但显然应该被回收。

标记-清除

  如果是只使用引用计数法的话,循环引用并不会被回收,它会一直驻留在内存中,就会造成了内存泄漏(内存空间在使用完毕后未释放)。标记-清除的出现打破了循环引用,也就是它只关注那些可能会产生循环引用的对象,显然,像是 PyIntObject、PyStringObject 这些不可变对象是不可能产生循环引用的,因为它们内部不可能持有其它对象的引用。Python 中的循环引用总是发生在 container 对象之间,也就是能够在内部持有其它对象的对象,比如 list、dict、class 等等。这也使得该方法带来的开销只依赖于 container 对象的的数量。

  标记清除(Mark—Sweep)算法是一种基于追踪回收技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC(Garbage Collection,垃圾回收)会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。原理与流程如下:

18-1

  • 首先初始所有对象标记为白色,并确定根节点对象(这些对象是不会被删除),标记它们为黑色(表示对象有效)。

  • 将有效对象引用的对象标记为灰色(表示对象可达,但它们所引用的对象还没检查),检查完灰色对象引用的对象后,将灰色标记为黑色。

  • 重复直到不存在灰色节点为止。最后白色结点都是需要清除的对象。

  标记-清除的缺点

  标记-清除机制所带来的额外操作和需要回收的内存块成正比。

分代技术

  分代回收是一种以空间换时间的操作方式,Python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python 将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是 3 个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。

  新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python 垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为 Python 的辅助垃圾收集技术处理那些容器对象。

坚持原创技术分享,您的支持将鼓励我继续创作!