当前位置: 首页 > 后端技术 > Python

大谈Python垃圾回收机制

时间:2023-03-26 17:02:56 Python

面试中经常被问到Python的垃圾回收是怎么实现的,其实就是一句话:引用计数器为主,分代回收和标记清除为辅。Garbagecollection1.1refchain在Python的C源码中有一个refchain的循环双向链表。在Python程序中一旦创建了一个对象,该对象就会被添加到refchain的链表中,以存储所有对象。name="pikachu"width=51.2referencecounterrefchain中的所有对象里面都有一个ob_refcnt用来保存当前对象的引用计数器name="pikachu"width=5nickname=name上面的代码表示有5和"pikachu"在内存值中,它们的引用计数器分别为1和2。当该值被多次引用时,不会在内存中重复创建数据,而是引用计数器+1。当对象被销毁时,引用计数器会同时为-1。如果引用计数器为0,则该对象会从refchain链表中删除,同时在内存中销毁(缓存等特殊情况除外)。name="pikachu"nickname=name#"pikachu"对象的引用计数器+1del对象"pikachu"的引用计数器-1defrun(arg):print(arg)run(nickname)#当first函数执行时,对象“皮卡丘”引用计数器+1,函数执行时,对象引用计数器-11但是还有一个BUG,当有循环引用的时候,会不能正常回收一些数据,比如v1=[11,22,33]#createalistobjectinrefchain,sincev1=object,sothelistreferenceobjectcounteris1.v2=[44,55,66]#在refchain中创建另一个listobject,因为v2=object,所以listobjectreferencecounter为1.v1.append(v2)#Appendv2tov1,则v2对应[44,55,66]对象的引用计数器加1,最后为2.v2.append(v1)#将v1添加到v1,则[11,22,33]对象校正对应v1加1,最后是2.delv1#Referencecounter-1delv2#Referencecounter-1对于上面的代码,执行del操作后,没有变量可以使用那两个list对象,但是由于对于循环引用的问题,他们的引用计数器不为0,所以他们的状态:从未使用过,从未销毁过。如果项目中这样的代码太多,就会消耗内存,直到内存耗尽,程序崩溃。1.3MarkClear&GenerationalRecyclingMarkClear:创建一个专门的链表来保存列表、元组、字典、集合、自定义类等对象,然后检查这个链表中的对象是否存在循环引用。如果是,则让两个引用计数器都为-1。分代回收:优化明确标记的链表,将那些可能有循环引用的对象拆分成3个链表,链表变成0/1/2三代,每一代可以存储对象和阈值,当达到阈值时,将对对应链表中的每个对象进行一次扫描,除了循环引用减1,引用计数器为0的对象被销毁。`//分代C源代码#defineNUM_GENERATIONS3structgc_generationgenerations[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},//第二代};0代,count表示0代链表中对象的个数,threshold表示0代链表中对象的阈值个数,如果超过则进行0代扫描检查。1代,count表示0代链表被扫描的次数,threshold表示0代链表被扫描次数的阈值。如果超过,则执行第1代扫描检查。2代,count表示第1代链表被扫描的次数,threshold表示第1代链表被扫描的次数阈值。如果超过,则执行第2代扫描检查。1.4缓存机制其实并没有那么简单粗暴,因为反复的创建和销毁会让程序的执行效率变低。Python中引入了“缓存机制”机制。例如:当引用计数器为0时,该对象并不会真正销毁,而是放入一个名为free_list的链表中,后面创建该对象时不会重新开内存,但之前的对象会存储在free_list中来重新设置内部值来使用。float类型,维护的free_list链表最多可以缓存100个float对象。*1.`v1=3.14#开辟内存存放float对象,并将对象添加到refchain列表中。`2.`print(id(v1))#内存地址:4436033488`3.`delv1#引用计数器-1,如果为0则从rechain链表中移除,该对象将不会被销毁,而是将对象添加到float的free_list中。`4.`v2=9.999#先去free_list中获取对象,并重新设置为9.999,只有free_list中的才重新开内存是空的。`5.`print(id(v2))#内存地址:4436033488`7.`#注意:当引用计数器为0时,会先判断free_list中的缓存数是否已满。如果缓存已满,则直接销毁该对象。`int类型,不是基于free_list,而是维护了一个small_ints链表来保存普通数据(小数据池),小数据池范围:-5<=value<257。即:当这个范围内的整数被复用时,内存不会被重新打开。v1=38#到小数据池small_ints中得到38整数对象,将对象加入refchain并让引用计数器+1。print(id(v1))#内存地址:4514343712v2=38#到小数据池small_ints中得到38个整型对象,refchain中对象的引用计数器加1。print(id(v2))#内存地址:4514343712#注意:-5~256在解释器启动和引用计数器初始化为1的时候已经加入到small_ints链表中,当代码中使用的值直接取fromsmall_ints就用它,给引用计数器加1。另外,small_ints中的数据引用计数器永远不会为0(初始化时设置为1),所以不会被销毁。str类型维护了unicode_latin1[256]链表,内部缓存了所有ascii字符,这样以后使用的时候不会重复创建。v1="A"print(id(v1))#Output:4517720496delv1v2="A"print(id(v1))#Output:4517720496#另外,Python在Mechanism内部也有驻留字符串,对于只包含字符串的字符串字母、数字、下划线(见源码Objects/codeobject.c),如果已经存在于内存中,则不会重新创建而是使用原来的地址(不会像free_list那样一直存在)内存存留,只有内存可以重复使用)。v1="wupeiqi"v2="wupeiqi"print(id(v1)==id(v2))#输出:Truelist类型,维护的free_list数组最多可以缓存80个列表对象。v1=[11,22,33]print(id(v1))#output:4517628816delv1v2=["LittlePig","Peppa"]print(id(v2))#output:4517628816tuple类型,维护一个free_list数组的容量为20。数组中的元素可以是链表,每个链表最多可以容纳2000个元组对象。元组的free_list数组存储数据时,根据元组能容纳的个数作为索引在free_list数组中找到对应的链表,加入到链表中。v1=(1,2)print(id(v1))delv1#因为元组个数为2,所以这个对象会缓存在free_list[2]的链表中。v2=("LittlePig","Peppa")#不会重新开内存,而是到free_list[2]对应的链表中去获取一个对象使用。print(id(v2))dict类型,维护的free_list数组最多可以缓存80个dict对象。v1={"k1":123}print(id(v1))#输出:4515998128delv1v2={"name":"吴佩琪","age":18,"gender":"男"}print(id(v1))#输出:45159981282C语言源码分析2.1两个重要结构#definePyObject_HEADPyObjectob_base;#definePyObject_VAR_HEADPyVarObjectob_base;//宏定义,包括previous和next,用于构造双向链表。(放入refchain链表时使用)#define_PyObject_HEAD_EXTRA\struct_object*_ob_next;\struct_object*_ob_prev;typedefstruct_object{_PyObject_HEAD_EXTRA//用于构造双向链表Py_ssize_tob_refcnt;//引用计数器结构对象_typeobject*;//数据类型}PyObject;typedefstruct{PyObjectob_base;//PyObject对象Py_ssize_tob_size;/*变量部分的项数,即:元素个数*/}PyVarObject;PyObject和PyVarObject这两个结构是基石,它们保存了其他数据类型的公共部分,例如:每种类型的对象在创建时都有PyObject中的4部分数据;list/set/tuple等由多个元素组成的对象在创建时有PyVarObject中的5部分数据。2.2常见的结构类型通常我们在创建对象的时候,本质上都是实例化一个相关类型的结构,并在里面存储值和引用计数器。float类型typedefstruct{PyObject_HEADdoubleob_fval;}PyFloatObject;int类结构_longobject{PyObject_VAR_HEADdigitob_digit[1];};/*Long(任意精度)整数对象接口*/typedefstruct_longobjectPyLongObject;/*显示在longintrepr.h*/str类型typedefstruct{PyObject_HEADPy_ssize_tlength;/*字符串中代码点的数量*/Py_hash_thash;/*哈希值;-1如果没有设置*/struct{unsignedintinterned:2;/*字符大小:-PyUnicode_WCHAR_KIND(0):*charactertype=wchar_t(16or32bits,dependingontheplatform)-PyUnicode_1BYTE_KIND(1):*charactertype=Py_UCS1(8bits,unsigned)*所有字符都在范围U+0000-U+00FF(latin1)*如果设置了ascii,则所有字符都在U+0000-U+007F(ASCII)范围内,否则至少有一个c字符在U+0080-U+00FF-PyUnicode_2BYTE_KIND(2)范围内:*字符类型=Py_UCS2(16位,无符号)*所有字符都在U+0000-U+FFFF(BMP)范围内*至少一个字符在U+0100-U+FFFF-PyUnicode_4BYTE_KIND(4)范围内:*字符类型=Py_UCS4(32位,无符号)*所有字符都在U+0000-U+10FFFF范围内*至少有一个字符在范围U+10000-U+10FFFF*/unsignedintkind:3;无符号整数紧凑型:1;无符号整数ascii:1;无符号整数就绪:1;无符号整数:24;}状态;wchar_t*wstr;/*wchar_t表示(空终止)*/}PyASCIIObject;typedefstruct{PyASCIIObject_base;Py_ssize_tutf8_length;/*utf8中的字节数,不包括*终止符\0。*/字符*utf8;/*UTF-8表示(空终止)*/Py_ssize_twstr_length;/*wstr中的代码点数,可能*代理项计为两个代码点。*/}PyCompactUnicodeObject;typedefstruct{PyCompactUnicodeObject_base;union{void*any;Py_UCS1*latin1;Py_UCS2*ucs2;Py_UCS4*ucs4;}数据;/*规范的、最小形式的Unicode缓冲区*/}PyUnicodeObject;listtypetypedefstruct{PyObject_VAR_HEADPyObject**ob_item;Py_ssize_t已分配;}PyListObject;元组类型typedefstruct{PyObject_VAR_HEADPyObject*ob_item[1];}PyTupleObject;dicttypedefstruct{PyObject_HEADPy_ssize_tma_used;PyDictKeysObject*ma_keys;PyObject**ma_values;数据扩展:在结构部分,你应该会发现str类型比较繁琐,因为python字符串在处理的时候需要考虑编码问题。内部是这样规定的(见源码结构):字符串只包含ascii,每个字符用1个字节表示,即latin1字符串如果包含中文等,每个字符用2个字节表示,即,如果ucs2字符串包含emoji等,每个字符用4个字节表示,即:ucs4写在末尾,问怎么清楚只有水源会来