@[toc]
Python垃圾回收
引用计数器为主,标记清除和分代回收为辅+缓存机制
1. 引用计数器
1.1 环状双向链表 refchain
在Python程序中创建的任何对象都会放在refchain
中
static PyObject refchain = {&refchain, &refchain}
在Python程序中创建的任何对象都会放在refchain
链表中
1 | str1 = "str" |
当进行上述操作时,Python内部会创建一些数据(上一个对象,下一个对象,类型,引用个数,元素个数)
include/object.h
1 |
|
2个结构体
- PyObject,此结构体中包含3个元素。
- _PyObject_HEAD_EXTRA,用于构造双向链表。
- ob_refcnt,引用计数器。
- ob_type,数据类型。
- PyVarObject,次结构体中包含4个元素(ob_base中包含3个元素)
- ob_base,PyObject结构体对象,即:包含PyObject结构体中的三个元素。
- ob_size,内部元素个数。
3个宏定义
- PyObject_HEAD,代指PyObject结构体。
- PyVarObject_HEAD,代指PyVarObject对象。
- _PyObject_HEAD_EXTRA,代指前后指针,用于构造双向队列。
Python中所有类型创建对象时,底层都是与PyObject和PyVarObject结构体实现,一般情况下由单个元素组成对象内部会使用PyObject结构体(float)、由多个元素组成的对象内部会使用PyVarObject结构体(str/int/list/dict/tuple/set/自定义类),因为由多个元素组成的话是需要为其维护一个 ob_size(内部元素个数)。
PyObject:float
PyVarObject:list、dict、tuple、set、int、str、bool
因为Python中的int是不限制长度的,所以底层实现是用的str,所以int也属于PyVarObject阵营。Python中的bool实际上是0和1,所以也是int,也属于PyVarObject阵营。
1.2 类型封装结构体
1 | // float类型 |
1 | data = 1.11 |
1.3 引用计数器
1 | v1 = 1.11 |
当python程序运行时,会根据数据类型的不同找到对应的结构体,根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain双线链表中。
每个对象中有ob_refcnt
引用计数器,值默认为1,当有其他变量引用对象时,引用计数器就会发生变化。
1 | a = 1 |
1 | a = 1 |
1 | # 创建对象并初始化引用计数器为1 |
1.4 循环引用的问题
1 | list1 = [1,2,3] |
list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。
对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。
2. 标记清除
目的:为了解决引用计数器循环引用的不足
实现:在Python的底层再维护一个链表,链表中专门放可能存在循环引用的对象(list/tuple/dict/set)
在Python内部某种情况
触发, 会去扫描可能存在循环应用的链表
中的每个元素, 检查是否有循环引用, 如果有则让双方的引用计数器-1; 如果是0则进行垃圾回收
问题:
- 什么时候扫描
- 可能存在循环引用的链表扫描代价大,每次扫描时间久
3. 分代回收
将可能存在循环应用的对象维护成3个链表:
- 0代:0代中对象的个数达到700个扫描一次
- 1代:0代扫描10次,则1代扫描一次
- 2代:1代扫描10次,则2代扫描一次
4. 小结
在Python中维护了一个refchain
的双向环状链表, 这个链表中存储程序创建的所有对象, 每种类型的对象中都有一个ob_refcnt
引用计数器的值, 引用个数+1, -1 , 最后当引用计数器变成0时会进行垃圾回收(对象销毁, 从refchain中移除)
但是. 在Python中对于那些可以有多个元素组成的对象可能会存在循环引用的问题, 为了解决这个问题Python引入了标记清除和分带回收, 在其内部维护了4个链表
- refchain
- 0代
- 1代
- 2代
在源码内部当达到各自的阈值时, 就会触发扫描链表进行标记清除的动作(有循环则各自-1)
Python缓存
1. 池
为了避免重复创建和销毁一些常见对象, Python建立了维护池
1 | # 启动解释器时, python内部帮我们创建: -5,-4...257 |
2. free_list
当一个对象的引用计数器为0时, 按理说应该回收, 但是内部不会直接回收, 而是将对象添加到free_list
链表中当缓存。以后再去创建对象时,不再重新开辟内存,而是直接使用free_list
1 | v1 = 1.11 # 开辟内存, 内存存储结构体中定义那几个值, 并存到refchain中 |
float类型,维护的free_list链表最多可缓存100个float对象。
1
2
3
4
5
6v1 = 3.14 # 开辟内存来存储float对象,并将对象添加到refchain链表。
print( id(v1) ) # 内存地址:4436033488
del v1 # 引用计数器-1,如果为0则在rechain链表中移除,不销毁对象,而是将对象添加到float的free_list.
v2 = 9.999 # 优先去free_list中获取对象,并重置为9.999,如果free_list为空才重新开辟内存。
print( id(v2) ) # 内存地址:4436033488
# 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。int类型,不是基于free_list,而是维护一个small_ints链表保存常见数据(小数据池),小数据池范围:
-5 <= value < 257
。即:重复使用这个范围的整数时,不会重新开辟内存。1
2
3
4
5v1 = 38 # 去小数据池small_ints中获取38整数对象,将对象添加到refchain并让引用计数器+1。
print( id(v1)) #内存地址:4514343712
v2 = 38 # 去小数据池small_ints中获取38整数对象,将refchain中的对象的引用计数器+1。
print( id(v2) ) #内存地址:4514343712
# 注意:在解释器启动时候-5~256就已经被加入到small_ints链表中且引用计数器初始化为1,代码中使用的值时直接去small_ints中拿来用并将引用计数器+1即可。另外,small_ints中的数据引用计数器永远不会为0(初始化时就设置为1了),所以也不会被销毁。str类型,维护
unicode_latin1[256]
链表,内部将所有的ascii字符
缓存起来,以后使用时就不再反复创建。1
2
3
4
5
6
7v1 = "A"
print( id(v1) ) # 输出:4517720496
del v1 v2 = "A"
print( id(v1) ) # 输出:4517720496 # 除此之外,Python内部还对字符串做了驻留机制,针对那么只含有字母、数字、下划线的字符串(见源码Objects/codeobject.c),如果内存中已存在则不会重新在创建而是使用原来的地址里(不会像free_list那样一直在内存存活,只有内存中有才能被重复利用)。
v1 = "wupeiqi"
v2 = "wupeiqi"
print(id(v1) == id(v2)) # 输出:Truelist类型,维护的free_list数组最多可缓存80个list对象。
1
2
3
4v1 = [11,22,33]
print( id(v1) ) # 输出:4517628816
del v1 v2 = ["武","沛齐"]
print( id(v2) ) # 输出:4517628816tuple类型,维护一个free_list数组且数组容量20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。
1
2
3
4
5v1 = (1,2)
print( id(v1) )
del v1 # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
v2 = ("武沛齐","Alex") # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
print( id(v2) )dict类型,维护的free_list数组最多可缓存80个dict对象。
1
2
3
4v1 = {"k1":123}
print( id(v1) ) # 输出:4515998128
del v1 v2 = {"name":"武沛齐","age":18,"gender":"男"}
print( id(v1) ) # 输出:4515998128
这个老师讲的通俗易懂, 非常棒, 更多详细的解释:https://pythonav.com/wiki/detail/6/88/
参考资料:
https://www.bilibili.com/video/BV1Ei4y1b7mo?p=2
https://my.oschina.net/hebianxizao/blog/57367
https://www.cnblogs.com/wupeiqi/articles/11507404.html