文章目录
python垃圾回收机制
基于C语言源码底层,了解垃圾回收机制的的实现。
- 引用计数器
- 标记清楚
- 分代回收
- 缓存机制
- python的c源码
总的就一句话:引用计数器为主、分代码回收和标记清除为辅
1. 引用计数器
1.1 环状双向链表 refchain
在python程序中创建的任何对象都会放在 refchain
链表中,也就是说他保存者所有创建的对象。
name = "武沛齐""
age = 18
hobby =["篮球","美女"]
内部会创建一些鼓据【上一个对象、下一个对象、类型、引用个数】
name = "小志"
new = name
内部会创建一些数据【上一个对象、下一个对象、类型、引用个数、val=18]
age = 18
内部会创建一些数据【上一个对象、下一个对象、类型、引用个数、items=元素、元素个数】
hobby =["篮球","美女"]
在C源码中如何体现每个对象中都有的相同的值:PyObject
结构体(4个值)。
有多个元素组成的对象: PyObject
结构体(4个值) + ob_size .
1.2 类型封装结构体
data = 3.14
内部会创建:
_ob_next = refchain中的上一个对象
_ob_prev = refchain中的下一个对象
ob_refcnt = 1 # 引用计数器
ob_type = float
ob_fval = 3.14
1.3 引用计数器
在refchain
中的所有对象内部都有一个ob_refcnt
用来保存当前对象的引用计数器,顾名思义就是自己被引用的次数,例如:
v1 = 3.14
v2 = 888
v3 = (1,2,3)
v4 = v3
# 3.14, 888, (1,2,3)这几个值得引用计数器分别为:1, 1, 2
当python程序运行时,会根据数据类型的不同找到其对应的结构体,根据结构体中的字段来进行创建相关的数据,然后将对象添加到 refchain
双线链表中。
在C源码中有两个关键的结构体: PyObject
、PyVarObject
。
每个对象中有ob_refcnt
就是引用计数器,值默认为1,当有其他变量引用对象时,引用计数器就会发生变化。
-
引用
a = 888 # 此时a 的计数为 1 b = a # b引用了a, a 的计数 +1,。
-
删除引用
a = 8888 b = a del b # 删除b变量:b对应对象引用计数器-1 del a # 删除a变量:a对应对象引用计数器 -1 # 当一个对象的引用计数器为0时, 以为这没有人在使用该对象,此时这个对象就被当做垃圾,是垃圾就要被处理,此时就要进行垃圾回收,避免浪费空间。 # 垃圾回收:1.对象从refchain链表中删除;2.讲对象销毁,内存归还。(此时就当做引用计数器为0对象就被销毁,会面学习缓存机制是,内部还有缓冲,并不会立即销毁。)
实例:
# 创建对象并初始化引用计数器为1
hero = "xiaozhi"
a = hero # 计数器+1
b = hero # +1
c = hero # +1 至此为4
# 再创建一个对象并初始化引用计数器为1
nb = "python"
# 创建的对象全部放在refchain链表中。如下图:
1.4 循环引用问题
现象实例
v1 = [11,22,33] # refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为1.
v2 = [44,55,66] #refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为1.
v1.append(v2) #把v2追加到v1中,则v2对应的[44,55,66]对象的引用计数器加1,最终为2.
v2.append(v1) #把v1追加到v2中,则v1对应的[11,22,33]对象的引用计数器加1,最终为2.
del v1 #引用计数器-1
del v2 #引用计数器-1
2. 标记清除
目的:为了解决引用计数器循环计数的不足
实现:在python的底层在维护一个链表,链表中专门放那些可能存在循环引用的对象,如:list /tuple/dict/set
在Python内部某种情况下触发,回去扫描可能存在循环应用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器-1;如果是0则垃圾回收.|
此时又有两个问题:
- 什么时候扫描?
- 可能存在循环引用的链表扫码代价大,每次扫扫描耗时久。
3. 分代回收
对标记清除中的链表进行优化,将那些可能存在循引用的对象拆分到3个链表,链表称为:0/1/2三代,每代都可以存储对象和阈值,当达到阈值时,就会对相应的链表中的每个对象做一次扫描,除循环引用各自减1并且销毁引用计数器为0的对象。
// 分代的C源码
#define NUM_GENERATIONS 3
struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0}, // 0代
{{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0}, // 1代
{{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0}, // 2代
};
将可能存在循环引用的对象维护成 3 个链表:
特别注意:0代和1、2代的threshold和count表示的意义不同。
- 0代,count表示0代链表中对象的数量,threshold表示0代链表对象个数阈值,超过则执行一次0代扫描检查。
- 1代,count表示0代链表扫描的次数,threshold表示0代链表扫描的次数阈值,超过则执行一次1代扫描检查。
- 2代,count表示1代链表扫描的次数,threshold表示1代链表扫描的次数阈值,超过则执行一2代扫描检查。
4. 小结
早python中维护了一个refchain
的双向环状链表,这个链表中存储了程序中创建的所有对象,,每种类型的对象都有一个ob_refcnt
引用计数器的值,引用个数+1 、-1, 最后当计数器变为0时就会被当做是垃圾,进行垃圾回收(对象销毁,refchain
中移除)。
但是,在python中对于那些可以有多个元素组成的对象存在循环引用的问题,为了解决这个问题,python有引入了标记清除和分代回收机制,在其内部维护了4个链表:
refchain
- 2代:10次
- 1代:10次
- 0代:700个
在源码内部当个达到各自阈值时,就会触发扫描链表进行标记清除的动作。
5. 缓存
池
上面大概了解了垃圾回收机制的过程,就是当对象的引用计数器为0 时,就会被当做垃圾回收并释放内存。但实际上并不完全是这样,因为一些对象需要反复创建和销毁,会导致程序执行效率变低,so,为了避免重复创建和销毁一些常见的对象,python便有了引入了缓存机制
。
例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list
的链表中,之后会再创建对象时不会在重新开辟内存,而是在free_list中将之前的对象来并重置内部的值来使用。
# 启动解释器是,python内部帮我们创建,一些常见得到对象,例如:-100....1024
v1 = 24 # 内部不会开辟内存空间来存储这些值,直接就去池中去取
v2 = 128 # 池中去值
v3 = 100 # 池中去值
- float类型,维护的free_list链表最多可缓存100个float对象。
v1 = 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
。即:重复使用这个范围的整数时,不会重新开辟内存。 -
str
类型,维护unicode_latin1[256]
链表,内部将所有的ascii字符
缓存起来,以后使用时就不再反复创建。 -
list类型,维护的free_list数组最多可缓存80个list对象。
-
tuple类型,维护一个
free_list
数组且数组容量 20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。 -
dict
类型,维护的free_list数组最多可缓存80个dict
对象。
基于C语言底层实现在这里就不多阐述了,毕竟我还没参透。。。
下面有详细介绍:
C语言底层实现详细过程请看:https://pythonav.com/wiki/detail/6/88/
本次内容的学习视频:https://www.bilibili.com/video/BV1dp4y1C7ja
(完 !)