引用计数器为主,标记清除和分代回收为辅+缓存机制。
提示:基本原理这句话基本覆盖python内存管理和垃圾回收的所有精髓。
一、基本流程
1、在python内部维护了一个双向环状链表,即refchain,引用计数器的值就是在此链表中体现。
2、这个引用次数的值是基于ob_refcnt来记录的,将其称为引用计数器。
3、引用计数器的值是随着对象的操作变化而变化的。例如创建了某变量,则ob_refcnt的值加1,变量释放时ob_refcnt的值减1.
4、 当ob_refcnt的值变为0时,进行垃圾回收(-5~257范围内的int数值类型除外,其默认引用计数器的值为1。最终删除对象时,将引用计数器还原为1)。
二、引用计数器
python内部创建的任何一个对象都会放到refchain链表中,在其内部创建一些数据(上一个对象:_ob_prev, 下一个对象:_ob_next, 类型:ob_type,引用计数器:ob_refcnt,元素ob_fval 等)。
1、 程序会根据数据类型的不同找到其相应的结构体,创建相关的数据,然后将其添加到refchain中。
2、 python的C源码中,有两个关键的结构体:1)PyObject 2) PyVarObject。一般的数据类型都是根据PyObject 创建的,但是有元素个数要求的都是根据 PyVarObject创建的。
3、需要注意,在python3.6版本中,int类型的数据其实也是根据 PyVarObject结构体创建的。
4、小栗子1:
a=999 # 引用计数器变为1
b=a # 引用计数器的值变为2
del b # 引用计数器值为1
del a # 引用计数器的值变为0,进行垃圾回收
注:垃圾回收原理:1)从refchain中将此对象进行删除。2)内存归还
5、小栗子2
v1=[11, 22, 33] # 在refchain中创建一个列表对象,由于V1=对象,所以列表对象引用计数器为1.
v2=[44, 55, 66] 3 refchain中再创建一个列表对象,由于v2=对象,所以列表对象引用计数器为1.
v1.append(v2) # 把v2追加到v1中,则v2对应的对象引用计数器加1,最终为2.
v2.append(v1) # 把v1追加到v2中,则v1对应的对象引用计数器加1,最终为2.
del v1 # ob_refcn的值t-1,则最终变为1
del v2 # ob_refcnt的值-1,则最终变为1
1) 出现的问题:本来已经删除了v1和v2,但是其引用计数器的值最终变为1,而不是0,导致不能进行垃圾回收。这就是循环引用问题,出现交叉感染。
2) 解决措施:标记清除
三、标记清除
1、除了refchain之外,再程序底层再创建一个链表,存储可能出现循环引用问题的这些对象
2、重新创建的链表在某种情况下(分代回收中解释是哪种情况)被触发。
3、重新创建的链表 通过扫描 可能存在循环引用的链表,检查是否存在循环引用的问题。如果有,则让双方的引用计数器同时-1.当引用计数器的值变为0时进行垃圾回收
4、问题:
- 什么时候扫描可能存在循环引用的链表
- 每次触发都要对链表进行扫描的话,其代价可能过大。
5、解决
- 使用 分代回收
四、分代回收
将可能存在循环引用的对象维护成3个链表,即0代,1代,2代。
- 0代链表:当可能存在循环引用的对象被创建时,都会被放入0代链表中。当其中的对象个数达到700时,扫描一次此链表。如果引用计数器的值为0,则其是垃圾,进行垃圾回收;如果不是垃圾,则将其添加到(升级到)1代链表中。总结为,0代中对象达到700则进行一次扫描。
- 1代链表:1代在每次接收了来自0代的对象后,会标记其扫描次数。当所标记的次数达到10时,1代链表的数据会被扫描。如果引用计数器的值为0,则其是垃圾,进行垃圾回收;如果不是垃圾,则将其添加到(升级到)2代链表中。总结为,0代扫描10次,则1代扫描1次。
- 2代链表:2代同一代,即1代共扫描10次后,2代扫描1次。
备注:在整个内存管理的过程中,python内部还会有一些优化机制(缓存与驻留机制),用于简化内存管理的操作。
五、缓存与驻留机制
(1)缓存机制
- 缓冲池(int)
- 针对-5~257范围内的int类型数据
- 启动解释器时,python内部自动创建-5~257范围内的值,其引用计数器的值默认为1.
- 每次创建这些范围内的对象时,都会从缓冲池中去取。
- 最终删除对象时,不会进行垃圾回收,而是将其引用计数器的值还原为1
2 . free_list(float/list/tuple/dict)
free_list:当一个对象的引用计数器为0时,内部不会直接回收,而是将对象添加到free_list链表中缓存。以后再去创建对象时,不会重新开辟内存,而是直接使用free_list
- float类型
v1=3.14 # 开辟内存来存储float对象,并将其添加到refchain链表中 del v1 # Ob_refcnt的值-1,如果为0则在refchain链表中移除,并将其添加到float类型的free_list中 v2 = 9.999 # 优先去free_list 中获取对象,free_list为空时才重新开辟内存
由于使用了缓存机制,则这里的v1和v2输出的地址值会是相同的 。float类型所维护的free_list最多可缓存100个float对象。
-
list类型
v1 = [000, 111, 222]
id_v1 = id(v1)
del v1
v2 = [222, 111, 999]
print(id(v2)==id_v1) # 则输出为True
list类型维护的free_list数组最多可缓存80个list对象。
- dict类型
v1 = {'k1': 123}
id_v1 = id(v1)
del v1
v2={"name":111, "age":222}
print(id(v2)==id_v1)
输出为True,最多存储80个
- tuple
free_list的结构为[0,1,2,3......19]:0代表当前位置可存储2000个空元组,1代表当前位置最多可存储2000个具有1个元素的元组;......19代表当前位置最多可存储具有19个元素的元组;
v1=(1, 2)
id_v1=id(v1)
v2=("aaaa", "2aaa")
print(id(v2)==id_v1)
输出为True
(2)驻留机制
- 维护unicode_latin1[256]链表,内部将所有的ascii字符缓存起来,以后使用时不再重复创建
- 针对于长度不大于20,只含有字母、数字、下划线的字符串类型。如果内存中已经存在符合该要求的字符串,则不会重新创建,而是直接使用。