垃圾回收算法(GC)
引用计数(Reference Counting)
所谓‘’万物皆对象‘’,python每一个对象的核心就是如下的结构体PyObject
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
在结构体中内部都有一个引用计数器(ob_refcnt)表示引用当前对象的数量
程序运行中会实时更新ob_refcnt的值,一旦引用计数器为0,内存直接释放
ob_refcnt加1的情况
- 对象被创建,例如a=2
- 对象被引用,b=a
- 对象被作为参数,传入到一个函数中
- 对象作为一个元素,存储在容器中,容器是指列表、字典、用户自定义类的对象、元组等
ob_refcnt减1的情况
- 对象别名被显示销毁 del
- 对象别名被赋予新的对象
- 一个对象离开他的作用域
- 对象所在的容器被销毁或者是从容器中删除对象
缺点:会产生循环引用问题,如下:
>>>a = { } #对象A的引用计数为 1
>>>b = { } #对象B的引用计数为 1
>>>a['b'] = b #B的引用计数增1
>>>b['a'] = a #A的引用计数增1
>>>del a #A的引用减 1,最后A对象的引用为 1
>>>del b #B的引用减 1, 最后B对象的引用为 1
这样a,b会一直驻留在内存中,就会造成了内存泄漏(内存空间在使用完毕后未释放)
为解决此问题,python引入了标记清除和分代回收两种GC机制
标记-清除(Mark and Sweep)
标记-清除机制是基于追踪回收(tracing GC)实现的,分为两个阶段
- 标记,会把所有的活动对象打上标记
- 清除,把没有标记的对象即非活动对象进行回收
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。(作者:天澄链接:https://juejin.im/post/5ca2471df265da307b2d45a3来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处)
把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。
缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
分代回收(Generational Collection)
分代回收是基于标记-清除机制的基础上,进行以空间换时间的操作方式
分代回收是基于这样的一个统计事实,对于程序,存在一定比例的内存块的生存周期比较短;而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。生存期较短对象的比例通常在 80%~90% 之间,这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。这样在执行标记-清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度。
Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),对应的是3个链表,它们的垃圾收集频率随着对象的存活时间的增大而减小。
新创建的对象都会分配在年轻代,**当某一世代中被分配的对象与被释放的对象之差达到某一阈值的时候,就会触发gc对某一世代的扫描。**Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
**值得注意的是当某一世代的扫描被触发的时候,比该世代年轻的世代也会被扫描。**也就是说如果世代2的gc扫描被触发了,那么世代0,世代1也将被扫描,如果世代1的gc扫描被触发,世代0也会被扫描。
查看和调整阈值
import sys; print('Python %s on %s' % (sys.version, sys.platform))
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
[Clang 6.0 (clang-600.0.57)] on darwin
import gc
gc.get_threshold()
(700, 10, 10)
gc.set_threshold(500,5,5)
gc.get_threshold()
(500, 5, 5)
总结:引用计数在处理容器对象时list、dict、tuple等会出现循环引用问题。标记-清除机制是为了解决容器循环引用问题,但是耗时较长,而分代回收则已空间换时间的方式优化标记-清除的问题。
参考链接:
https://juejin.im/post/5cdb8216e51d456e781f20e0
https://andrewpqc.github.io/2018/10/08/python-memory-management/#more
https://juejin.im/entry/5ac4746a6fb9a028d700d13b
https://juejin.im/post/5ca2471df265da307b2d45a3