Python垃圾回收机制主要以引用计数为主,标记清除, 分代收集为辅助
一、引用计数
引用计数的原理:
每个对象维护一个ob_ref字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_ref加1,每当该对象的引用失效时计数ob_ref减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。
当发生以下四种情况的时候,该对象的引用计数器+1
- 对象被创建 a=14
- 对象被引用 b=a
- 对象被作为参数,传到函数中 func(a)
- 对象作为一个元素,存储在容器中 List={a,”a”,”b”,2}
与上述情况相对应,当发生以下四种情况时,该对象的引用计数器-1
- 当该对象的别名被显式销毁时 del a
- 当该对象的引别名被赋予新的对象, a=26
- 一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
- 将该元素从容器中删除时,或者容器被销毁时。
引用计数法有很明显的优点:
- 高效、实现逻辑简单、具备实时性,一旦一个对象的引用计数归零,内存就直接释放了。不用像其他机制等到特定时机。
- 运行期没有停顿,将垃圾回收随机分配到运行的阶段,处理回收内存的时间分摊到了平时,正常程序的运行比较平稳
缺点:
- 维护引用计数消耗资源, 空间
- 无法解决循环引用的问题(会导致内存泄漏)。A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收。 例如:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
为了解决这两个致命弱点,Python又引入了以下两种GC机制。
二、标记清除
Python采用了“标记-清除”(Mark and Sweep)算法,解决容器对象可能产生的循环引用问题。(注意,只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义类的对象、元组等。而像数字,字符串这类简单类型不会出现循环引用。作为一种优化策略,对于只包含简单类型的元组也不在标记清除算法的考虑之列)
跟其名称一样,该算法在进行垃圾回收时分成了两步,分别是:
- 标记阶段,GC会把所有活动对象打上标记,这些活动对象就如同一个点,他们之间的引用关系构成边,最终构成一个有向图(链表)。(遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达)
- 清除阶段,从根对象出发,再次遍历整个图,如果发现某个对象没有标记为可达,则就将其清除回收。
Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
三、分代回收
分代回收建立在标记清楚的基础之上,是一种以时间换取空间的操作方式。标记清楚可以回收循环引用的垃圾,但是,回收的频率是需要控制的,如果时时刻刻做标记清楚,那么Python程序就会变得很慢。因此需要合理的触发机制。
分代回收,将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代)。新生的对象放在0代,如果一个对象能在0代的垃圾回收过程中存活下来,GC就把它放在1代,如果1代的对象在第一代的垃圾回收机制中存活下来,就把它放到2代。
gc的扫描在什么时候会被触发呢?答案是当某一世代中被分配的对象与被释放的对象之差达到某一阈值的时候,就会触发gc对某一世代的扫描。代数越高,回收的频率约低。默认的阀值为(700, 10, 10),可以自定义设置,但是不建议改。
- 当分配的对象的个数减去释放对象的个数差值大于700时,就会产生一次0代回收
- 10次零代回收会导致一次1代回收
- 10次一代回收会导致一次2代回收
值得注意的是当某一代的扫描被触发的时候,比它年轻的代也会被扫描。也就是说如果2代的gc扫描被触发了,那么0代和1代也将被扫描,如果1代的gc扫描被触发,0代也会被扫描。
这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。这样在执行标记-清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度。
四、总结
总体来说,在Python中,主要通过引用计数进行垃圾回收;通过 “标记-清除” 解决容器对象可能产生的循环引用问题;通过 “分代回收” 以空间换时间的方法提高垃圾回收效率。
参考:一文搞定Python垃圾回收机制:https://www.jianshu.com/p/b0bc1a162933