1.Python的垃圾回收机制原理
Python无需我们手动回收内存,它的垃圾回收是如何实现的呢?
引用计数为主(缺点:循环引用无法解决)
引入标记清除和分代回收解决引用计数问题
引用计数为主+标记清除和分代回收为辅
垃圾回收(GC)
(1)引用计数
python里面一切皆对象,比如创建一个列表 [1],这个一个list对象。实际上Python的C语言实现中对于每个对象它都有几个字段,如它是什么类型以及引用计数即 ref 的值等,引用计数ref 就是C语言实现里面一个int值,用来计算有多少个变量在引用它。
a = [1] # [1]对象引用计数增加1,ref=1
b = a # [1]对象引用计数增加1,ref=2
b = None # [1]对象引用计数减少1,ref=1
del a # [1]对象引用计数减少1,ref=0
a = [1],当把列表 [1] 赋值给 a 的时候,它的引用计数就会增加1,此时列表 [1] 对象的引用计数ref=1 ; b = a 又把 a 赋值给 b ,a和b 同时引用了列表[1]对象,ref又增加1,此时 ref =2。继续执行 b = None, 让b指向None,这个时候它就不会指向原来的列表[1]对象,列表[1]对象的引入计数就会减少1,又变成ref=1。执行del a ,引用计数就会减少1,这个时候 ref = 0。当对象的引用计数为0就可以回收掉,
什么时候引用计数减少,比如让b 不在指向[ 1],这时候引用计数就会减少,还可以使用Python的del 操作符,del a 也会减少对象[1]引用计数。
注意:del 作用就会减少对象引用计数,并不是销毁对象。只有当引用计数为0的时候,Python解释器才回去把对象占用的内存回收掉。
// object.h
struct _object {
Py_ssize_t ob_refcnt; # 引用计数值
}PyObject;
① 什么时候引用计数增加呢?
- 对象创建 a = 1
- 对象被引用 b = a
- 对象作为参数传递 func(a)
- 对象存储在容器中 l = [a]
② 什么时候引用计数会减少呢?
- 显示使用 del a
- 引用指向了别的对象 b=None
- 离开的对象的作用域(比如函数执行结束)
- 从一个容器移除对象或者销毁容器
>>> import sys
>>> a = [1,2]
>>> sys.getrefcount(a) # 查看a的引用计数
2 # 创建对象,a引用增加1,调用函数也会增加引用计数,因此这里是2
>>> b = 1
>>> sys.getrefcount(1)
189 # 跟python底层实现有关,小正数对象池,不只是b引用1,内部实现有很多引用它
# 所以结果和你想的不太一样
(2)引用计数无法解决循环引用问题
循环引用解决不了可能会造成内存泄漏致命问题,因为循环引用致命的缺陷导致Python还需要别的机制来去增强它的垃圾回收功能。
循环引用
a = [1] # 对象[1]引用计数增加1,ref=1
b = [2] # 对象[2]引用计数增加1,ref=1
a.append(b) # b被a引用,对象[2]引用计数增加1,ref=2
b.append(a) # a又被b引用,对象[1]引用计数增加1,ref=2
del a # 对象[1]引用计数减少1,ref=1
del b # 对象[2]引用计数减少1,ref=1
# 两个对象互相引用之后引用计数无法归零
a = [1],[1]赋值给a, a指向[1]对象,此时 [1] 的引用计数为 ref=1,然后执行 b = [2], b指向了[2]列表对象, 此时 [2] 的引用计数为 ref=1。执行操作 a.append(b) ,这个时候 b被 a 引用,此时 [2] 对象的引用计数 ref=2,在执行b.append(a), 这时候 a 又被b引用,此时 [1] 对象的引用计数 ref=2。这时候会出现互相引用即循环引用。
执行del a 和del b 这时候把 a 和 b去掉,对象[1] 和 [2] 的引用计数都变成 ref=1,发现最后引用计数都没有变成0,而且两个对象出现互相引用这就出现了循环引用的问题,并且始终没有办法销毁 [1]和 [2] 这两个列表对象。循环引用的问题导致这两个对象始终没有办法去回收。
(3)标记清除(Mark and Sweep)
图解:标记清除的原理就是从垃圾回收的根对象(GC root 标记为红色的点) 开始不断查找可达对象。如左边图可达对象其实就是一个有向图,线表示引用关系,从图中可以看到有三个蓝色点是从根节点不可达的点(孤立点),分别是被四个点包裹的中间点和左下角的两个相连的点。如右图标记清除就是把不可达的点标灰,对于可达的点全部标绿。凡是标灰的点就认为没有对象去引用它就可以把它清除掉,这就是标记清除的原理。
(4)分代回收
Python把对象的生命周期分为三代,分别是第0代、第1代、第2代。每一代使用双向链表来标记这些对象。Python把一开始创建的新的对象称为第0代,比如 a ↔ b ↔ c ↔ d。每隔一定时间Python就会执行一个操作对第0、1、2代分别执行标记回收。每一代都会有预值,每隔多少时间就会清除第0代,每隔多少时间就会清除第1代,每隔多少时间就会清除第2代。每一代执行完标记回收之后剩下的对象没有回收的就会换移到下一代。
>>> import gc
>>> gc.get_threshold()
(700, 10, 10) # 第一个700为第0代预值,第二个10为第1代预值,第三个10为第2代预值
get_threshold() -> (threshold0, threshold1, threshold2)
当每一代达到预值,就会触发GC操作,如第0代链表长度达到700个对象触发一次GC操作,也就是把第0代执行一次标记回收。