白酒网站建设,网站 68,电子工程专业大学排名,蓝色网站配色垃圾回收#xff1a;用通俗点的语言解释就是内存管理和垃圾回收的过程.
大管家refchain
在Python的C源码中有一个名为refchain的环状双向链表#xff0c;这个链表就比较厉害了#xff0c;因为Python程序中一旦创建对象都会把这个对象添加到refchain这个链表中。也就是说他…垃圾回收用通俗点的语言解释就是内存管理和垃圾回收的过程.
大管家refchain
在Python的C源码中有一个名为refchain的环状双向链表这个链表就比较厉害了因为Python程序中一旦创建对象都会把这个对象添加到refchain这个链表中。也就是说他保存着所有的对象。例如
age 18
hobby python 引用计数器
在refchain中的所有对象内部都有一个ob_refcnt用来保存当前对象的引用计数器顾名思义就是自己被引用的次数例如
age 18
hobby python
hobby_1 hobby
上述代码表示内存中有 18 和 “python” 两个值他们的引用计数器分别为1、2 。 当值被多次引用的时候不会在内存中重复创建数据而是引用计数器1 。 当对象被销毁得时候同时会让引用计数器-1 如果引用计数器为0则将对象从refchain链表中摘除同时在内存中进行销毁暂不考虑缓存等特殊情况。
age 18
number age #对象18的引用计数器 1
del age #对象18的引用计数器 - 1def run(arg):print(arg)run(number) #刚开始执行函数时对象18引用计数器 1当函数执行完毕之后对象18引用计数器 - 1 。
num_list [11,22,number] #对象18的引用计数器 1
当发生以下四种情况的时候该对象的引用计数器1
对象被创建 a14对象被引用 ba对象被作为参数,传到函数中 func(a)对象作为一个元素存储在容器中 List{a,”a”,”b”,2}
与上述情况相对应当发生以下四种情况时该对象的引用计数器-1
当该对象的别名被显式销毁时 del a当该对象的引别名被赋予新的对象 a26一个对象离开它的作用域例如 func函数执行完毕时函数里面的局部变量的引用计数器就会减一但是全局变量不会将该元素从容器中删除时或者容器被销毁时。
当指向该对象的内存的引用计数器为0的时候该内存将会被Python虚拟机销毁
标记清除分代回收
基于引用计数器进行垃圾回收非常方便和简单但他还是存在循环引用的问题导致无法正常的回收一些数据例如
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追加到v1中则v1对应的[11,22,33]对象的引用计数器加1最终为2.del v1 #引用计数器-1
del v2 #引用计数器-1
对于上述代码会发现执行del操作之后没有变量再会去使用那两个列表对象但由于循环引用的问题他们的引用计数器不为0所以他们的状态永远不会被使用、也不会被销毁。项目中如果这种代码太多就会导致内存一直被消耗直到内存被耗尽程序崩溃。
为了解决循环引用的问题引入了标记清除技术专门针对那些可能存在循环引用的对象进行特殊处理可能存在循环应用的类型有列表、元组、字典、集合、自定义类等那些能进行数据嵌套的类型。
标记清除创建特殊链表专门用于保存 列表、元组、字典、集合、自定义类等对象之后再去检查这个链表中的对象是否存在循环引用如果存在则让双方的引用计数器均 - 1 。
分代回收对标记清除中的链表进行优化将那些可能存在循引用的对象拆分到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代
};
特别注意0代和1、2代的threshold和count表示的意义不同。
0代count表示0代链表中对象的数量threshold表示0代链表对象个数阈值超过则执行一次0代扫描检查。1代count表示0代链表扫描的次数threshold表示0代链表扫描的次数阈值超过则执行一次1代扫描检查。2代count表示1代链表扫描的次数threshold表示1代链表扫描的次数阈值超过则执行一2代扫描检查。
情景模拟
根据C语言底层并结合图来讲解内存管理和垃圾回收的详细过程。
第一步当创建对象age19时会将对象添加到refchain链表中。 第二步当创建对象num_list [11,22]时会将列表对象添加到 refchain 和 generations 0代中。 第三步新创建对象使generations的0代链表上的对象数量大于阈值700时要对链表上的对象进行扫描检查。
当0代大于阈值后底层不是直接扫描0代而是先判断2、1是否也超过了阈值。
如果2、1代未达到阈值则扫描0代并让1代的 count 1 。如果2代已达到阈值则将2、1、0三个链表拼接起来进行全扫描并将2、1、0代的count重置为0.如果1代已达到阈值则讲1、0两个链表拼接起来进行扫描并将所有1、0代的count重置为0并让2代的 count 1
对拼接起来的链表在进行扫描时主要就是剔除循环引用和销毁垃圾详细过程为
扫描链表把每个对象的引用计数器拷贝一份并保存到 gc_refs中保护原引用计数器。再次扫描链表中的每个对象并检查是否存在循环引用如果存在则让各自的gc_refs减 1 。再次扫描链表将 gc_refs 为 0 的对象移动到unreachable链表中不为0的对象直接升级到下一代链表中。处理unreachable链表中的对象的 析构函数 和 弱引用不能被销毁的对象升级到下一代链表能销毁的保留在此链表。 析构函数指的就是那些定义了__del__方法的对象需要执行之后再进行销毁处理。弱引用最后将 unreachable 中的每个对象销毁并在refchain链表中移除不考虑缓存机制。
至此垃圾回收的过程结束。
1.5 缓存机制
从上文大家可以了解到当对象的引用计数器为0时就会被销毁并释放内存。而实际上他不是这么的简单粗暴因为反复的创建和销毁会使程序的执行效率变低。Python中引入了“缓存机制”机制。
例如引用计数器为0时不会真正销毁对象而是将他放到一个名为 free_list 的链表中之后会再创建对象时不会在重新开辟内存而是在free_list中将之前的对象来并重置内部的值来使用。 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。即重复使用这个范围的整数时不会重新开辟内存。 v1 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字符缓存起来以后使用时就不再反复创建。 v1 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)) # 输出True list类型维护的free_list数组最多可缓存80个list对象。 v1 [11,22,33]
print( id(v1) ) # 输出4517628816
del v1
v2 [武,沛齐]
print( id(v2) ) # 输出4517628816 tuple类型维护一个free_list数组且数组容量20数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时是按照元组可以容纳的个数为索引找到free_list数组中对应的链表并添加到链表中。 v1 (1,2)
print( id(v1) )
del v1 # 因元组的数量为2所以会把这个对象缓存到free_list[2]的链表中。
v2 (武沛齐,Alex) # 不会重新开辟内存而是去free_list[2]对应的链表中拿到一个对象来使用。
print( id(v2) )
参考
pythonav资源分享
Pythonn内存管理以及垃圾回收机制 - 武沛齐 - 博客园