DAY 18. python垃圾回收机制
python GC主要有三种方式
其中,以引用计数为主。
18.1 引用计数(Reference Counting)
《寻梦环游记》中说,人一生会经历两次死亡,一次是肉体死的时候,另一次是最后一个记得你的人也忘了你时,当一个人没有人记得的时候,才算真的死亡。垃圾回收也是这样,当最后一个对象的引用死亡时,这个对象就会变成垃圾对象。
引用计数的原理是在每次创建对象时都添加一个计数器,每当有引用指向这个对象时,该计数器就会加1,当引用结束计数器就会减1,当计数器为0时,该对象就会被回收。
python 中所有对象所共有的数据成员由一个叫做pyobject的结构体来保存
typedef struct _object {
/* 宏,仅仅在Debag模式下才不为空 */
_PyObject_HEAD_EXTRA
/* 定义了一个 Py_ssize_t 类型的 ob_refcnt 用来计数 */
Py_ssize_t ob_refcnt;
/* 类型 */
struct _typeobject *ob_type;
} PyObject;
里面的ob_refcnt就是垃圾回收用到的计数器,而Py_ssize_t是整数
pyobject中保存的是python对象中共有的数据成员,所以python创建的每一个对象都会有该属性。
在python中可以使用from sys import getrefcount
来查看引用计数的值,但一般这个值会比期望的ob_refcnt高,应为它会包含临时应用以作为getrefcount的参数
以下情况ob_refcnt加一
- 创建对象
- 引用对象
- 作为参数传递到函数中
- 作为成员存储在容器中
from sys import getrefcount
foo: int = 1
print(getrefcount(foo)) # 91 应为包含临时引用,所以会比预期的高很多
bar: int = foo
print(getrefcount(foo)) # 92 增加了一个foo的引用,所以计数加一
List = []
List.append(foo)
print(getrefcount(foo)) # 93 作为成员存储在容器中,计数加一
def Foo(*agrs):
print(getrefcount(foo)) # 95 作为参数传递给了函数计数加一,实参与形参的赋值使计数加一
Foo(foo)
print(getrefcount(foo)) # 函数生命周期结束,计数减2
以下情况,计数减一:
- 当该对象的别名被显式销毁时
- 该对象的别名被赋予新值时
- 离开作用域时
- 从容器中删除时
del bar
print(getrefcount(foo)) # 92 对象的别名被显式销毁
List.pop()
print(getrefcount(foo)) # 91 从容器中删除
foo2: int = foo
foo2 = 2
print(getrefcount(foo)) # 91 别名被赋予新值
当计数被减为0时,该对象就会被回收
class MyList(list):
def __del__(self):
print('该对象被回收')
s = MyList()
s = [] # s是MyList实例对象唯一的引用,s指向别的对象,MyList的这个实例对象就会被立刻回收
print('end')
# 该对象被回收
# end
优点:
- 实现简单
- 内存回收及时,只要没有引用立刻回收
- 高效对象有确定生命周期
缺点:
- 维护计数器占用资源
- 无法解决循环引用问题
# 循环引用
class MyList(list):
def __del__(self):
print('该对象被回收')
if __name__ == '__main__':
a = MyList()
b = MyList()
a.append(b)
b.append(a)
del a
del b
print('程序结束')
# 程序结束
# 该对象被回收
# 该对象被回收
a和b相互引用,造成a,b的计数始终大于0,这样就无法使用引用计数的方法处理垃圾,针对这种情况,python使用另外一种GC机制——标记清除来回收垃圾。
18.2 标记清除(Mark-Sweep)
标记清除就是为解决循环引用产生的,应为它造成的内存开销较大,所以在不会产生循环引用的对象上是不会使用的。
- 哪写对象会产生循环引用?
只有能“引用”别的对象,才会产生循环引用,那些int,string等是不会产生的,只有“容器”,类似list,dict,class之类才可能产生,也只有这类对象才可能使用标记清除机制。
过程:
- 去环
- 计数为0的加入生存组,不为零的加入死亡组
- 生存组中的元素作为root,root的可达节点从死亡组中提出
- 回收死亡组中的对象
原理:
from sys import getrefcount
class MyList(list):
def __del__(self):
print('该对象被回收')
a = MyList()
b = MyList()
a.append(b)
b.append(a)
print(f'a的引用计数{getrefcount(a)}')
print(f'b的引用计数{getrefcount(b)}')
del a
print(f'del a的引用计数{getrefcount(b[0])}')
c = MyList()
d = MyList()
c.append(d)
d.append(c)
print(f'c的引用计数{getrefcount(c)}')
print(f'd的引用计数{getrefcount(d)}')
del c
del d
print('end')
这是一开始a,b 的情况
他们的计数都是2,cd也一样,使用del语句会断开变量ab与MyList()内存之间的联系
这个时候就该标记清除上场了,由于a还存在,而a中引用了b,cd相互引用但都通过del显式清除了,所以经过标记清除,ab会被保留,cd会被清除。
标记清除的第一步是“标记”,通过两个容器来实现————生存容器和死亡容器,python首先会检测循环引用,这时会将所有对象的计数复制一个副本以避免破坏真实的引用计数值,然后检查链表中每个相互引用的对象,把这些对象的计数都减一,这一步叫做去环。
上面ab,cd都相互引用,经过del之后,a的计数依旧是2,bcd的计数是1,去环以后a的计数是1,bcd计数为0。
经过去环以后,将所有计数为0的值(bcd)加入死亡容器,不为0的(a)加入生存容器,这时还不能直接清除死亡容器中的对象,需要二审,先遍历生存容器中的对象,把每一个生存容器中的值作为root object,根据该对象的引用关系建立有向图,可达节点就标记为活动对象,不可达节点就为非活动对象(就是查看生存容器中是否引用了死亡容器中的对象,如果有,就把这个对象从死亡容器解救到生存容器)。
这里a引用了死亡容器中的b,所以b会被解救。
最后,死亡容器中的对象会被清除。
- 什么时候进行标记清除
标记清除并不像引用计数那样是实时的,而是等待占用内存到达GC阈值的时候才会触发
18.3 分代回收
上面说了标记回收通过生存和死亡两个容器来实现,但这只是为了方便理解说的,在真实情况下,标记清除是依赖分代回收计数完成的。
首先,我们在python中创建的每一个对象都会被收纳进一个链表中,python称其为零代(Generation Zero)经过检测循环引用,会按照规则减去有循环引用的节点的计数值,这时候部分节点的计数值大于0,也有部分节点计数值等于0,大于0的节点会被放入二代,等于0的节点经过“白障算法(write barrier)”就是上面说的二审,通过的就会放在零代,不通过的就会被清除释放。一段时间后,使用同样的算法遍历一代链表,计数大于0的放入二代链表,等于0的进行白障算法检测,通过留在一代,否则释放,python中只有这三代链表,根据 “弱代假说”(新生的对象存活时间比较短,年老的对象存活时间一般较长)python GC 会将主要精力放在零代上,而触发回收则是根据GC阈值决定的,GC阈值是被分配对象的计数值与被释放对象的计数值之间的差异,一旦这个差异超过阈值,就会触发零代算法,回收垃圾把剩余对象放在一代,一代也类似,但随着代数增加,阈值会提高(弱代假说),也就是零代的垃圾回收最频繁,一代次之,二代最少。
18.4 总结
- GC的工作:
- 为新创建的对象分配内存
- 识别垃圾对象
- 回收垃圾对象的内存
- 什么是垃圾:
- 没有对象引用
- 只相互引用,孤岛
- python GC机制:
python GC机制由三部分组成:引用计数,标记清除,分代回收,其中引用计数为主。- 引用计数:python所有对象的共同属性由pyobject结构体保存,该结构体中有一个int类型的成员ob_refcnt用来实现引用计数。计数为0时对象为垃圾对象,回收内存。
- 计数加一的情况:创建对象,对象作为函数参数传递,对象作为成员保存到容器中,对象增加了一个引用
- 计数减一的情况:通过del显式删除对象,引用指向None或别的对象,从容器中弹出,跳出作用域如函数生命结束
- 优点:实现简单,实时回收内存
- 缺点:无法解决循环引用问题,开销大
- 标记清除和分代回收:是为了解决引用计数无法回收相互引用的问题
- 作用对象:只作用于可能产生相互引用的“容器对象”如list,dict,class
- 处理过程:创建对象->加入零代链表->到达阈值->检测循环引用->循环引用的节点计数减少->计数大于0的加入一代链表,小于零的->白障->在一代链表中有他的引用->不清理,保留,没有引用,清理释放内存。
- 弱代假说:新生的对象一般存活时间较短,年老对象存活时间较长
- 引用计数:python所有对象的共同属性由pyobject结构体保存,该结构体中有一个int类型的成员ob_refcnt用来实现引用计数。计数为0时对象为垃圾对象,回收内存。