1. 垃圾回收机制
1.1 目的
Python 垃圾回收机制(Garbage Collection)主要使用引用计数(reference counting)来跟踪和回收垃圾。在引用计数的基础上,使用“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用问题;使用“分代回收”(generation collection)以空间换时间的方法提高垃圾回收效率。
2. 引用计数
Python垃圾回收主要以引用计数为主,分代回收为辅。引用计数法的原理是每个对象维护一个引用计数字段,用来记录当前对象被引用的次数,也就是来追踪到底有多少引用指向了这个对象,当发生以
下四种情况的时候,该对象的引用计数器+1:
- 对象被创建 (例如:a = 1)
- 对象被引用(例如:b = a)
- 对象被作为参数,传到函数中(例如:function(a))
- 对象作为一个元素,存储到容器中(例如:List = [0,1,2])
当发生以下四种情况时,该对象的引用计数器-1:
- 当该对象的别名被显式销毁时 (例如:del a)
- 当该对象的引别名被赋予新的对象 (a=26)
- 一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
- 将该元素从容器中删除时,或者容器被销毁时。
当指向该对象的内存的引用计数器为0的时候,该内存将会被Python虚拟机销毁。
2.1 引用计数的优缺点
引用计数法有很明显的优点:
- 高效
- 实时性:一旦没有引用,内存就直接释放了
- 对象有确定的生命周期
- 易于实现
原始的引用计数法也有明显的缺点:
- 维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比,而不像mark and sweep等基本与回收的内存数量有关;
- 无法解决循环引用的问题。A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收。
a = [1]
b = [2]
a.append(b)
b.append(a)
print(a)
print(b)
"""
运行结果:
[1, [2, [...]]]
[2, [1, [...]]]
"""
为了解决这两个致命弱点,Python又引入了以下两种GC机制。
3. 标记清除
标记清除Mark-Sweep是针对循环引用问题的回收机制,作用的对象是容器类型的对象(比如:list、set、dict等)。
它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。
标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。
标记-清除机制,顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收)。如图:
首先初始所有对象标记为白色,并确定根节点对象(这些对象是不会被删除),标记它们为黑色(表示对象有效)。将有效对象引用的对象标记为灰色(表示对象可达,但它们所引用的对象还没检查),检查完灰色对象引用的对象后,将灰色标记为黑色。重复直到不存在灰色节点为止。最后白色结点都是需要清除的对象。
标记清除的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
4. 分代回收
分代回收是建立在标记清除基础上的一种辅助回收容器对象的GC机制。Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代。Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),垃圾收集频率随对象的存活时间的增大而减小。
新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象