当前位置: 首页 > 图灵资讯 > 行业资讯> 大话Python的垃圾回收机制

大话Python的垃圾回收机制

来源:图灵python
时间: 2025-03-04 22:13:26

垃圾回收

1.1 refchain

Python的C源码中有一个refchain环形双向链表,一旦在Python程序中创建对象,就会将对象添加到refchain链表中,以保存所有对象。

name="皮卡丘"
width=5

1.2引用计数器

在refchain中,所有对象都有一个ob_refcnt来保存当前对象的引用计数器

name="皮卡丘"
width=5
nickname=name

上述代码表示内存中有5和“皮卡丘”两个值,其引用计数器分别为1、2

当值被多次引用时,它不会在内存中重复创建数据,而是引用计数器+1。当对象被销毁时,引用计数器-1。如果引用计数器为0,则从refchain链表中删除对象,并在内存中销毁(暂时不考虑缓存等特殊情况)。

name="皮卡丘"nickname=name#“对象”皮卡丘“引用计数器+1delname对象”"皮卡丘"引用计数器-1defrun(arg):
print(arg)

run(nickname)#当函数开始执行时,对象“皮卡丘”引用计数器+1,当函数完成时,对象引用计数器-1name_list=["张三","法外狂徒",name]#“皮卡丘”引用计数器+1

然而,仍然有一个bug。当出现循环引用时,一些数据将无法正常回收,例如

在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.
delv1#引用计数器-1delv2#引用计数器-1delv2#

对于上述代码,在执行del操作后,没有变量将使用这两个列表对象,但由于循环引用的问题,他们的引用计数器不是0,所以他们的状态:永远不会被使用或销毁。如果项目中有太多这样的代码,它将导致内存消耗,直到内存耗尽,程序崩溃。

1.3标记清除&分代回收

标记清除:专门用于保存特殊链表 列表、元组、字典、集合、自定义等对象,然后检查链表中的对象是否有循环引用。如果存在,请双方引用计数器 - 1 。分代回收:优化标记清晰的链表,将可能存在循环引用的对象分成三个链表,链表成为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},/2代};

0代,count表示0代链表中对象的数量,threshold表示0代链表中对象的数量阈值,超过0代扫描检查。

第一代,count表示0代链表扫描次数,threshold表示0代链表扫描次数阈值,超过1代扫描检查。

第二代,count表示1代链表扫描次数,threshold表示1代链表扫描次数阈值,超过12代扫描检查。

1.4缓存机制

事实上,他并不是那么简单和粗糙,因为反复的创建和销毁会降低程序的执行效率。Python引入了“缓存机制”机制。例如,当引用计数器为0时,它不会真正摧毁对象,而是将其放入一个名为fre_list的链表中,然后在创建对象时不会重新打开内存,而是将以前的对象重置到fre_list中使用。

float类型,维护free_list链表最多可缓存100个float对象。

1.`v1=3.14#打开内存存储float对象,并将对象添加到refchain链表中。`
2.`print(id(v1)#内存地址:44360348`
3.`delv1#引用计数器-1,如果为0,则在rechain链表中删除,不销毁对象,而是将对象添加到floatfre_中list.`
4.`v2=9.999#优先在free_list中获取对象,并将其重置为9.999。如果fre_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)#内存地址: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
delv1
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)#输出:451762816
delv1
v2=["小猪","佩奇"]
print(id(v2)#输出:451762816

tuple类型,维护free_list数组和20个数组容量,数组中的元素可以是链表,每个链表最多可以容纳2000个元组对象。在存储数据时,元组的fre_list数组根据元组可容纳的数量找到fre_list数组中对应的链表,并添加到链表中。

v1=(1,2)
print(id(v1)delv1#因元组的数量为2,因此将此对象缓存到free_list链表中的[2]。
v2=("小猪","佩奇")#不会重新开放内存,而是去free_list[2]在相应的链表中使用一个对象。
print(id(v2))

dict类型,维护free_list数组最多可缓存80个dict对象。

v1={"k1":123}
print(id(v1)#输出:451598128
delv1
v2={"name":"武沛齐","age":18,"gender":"男"}
print(id(v1)#输出:451598128

2 C语言源码分析

2.1两个重要的结构体

#definePyObject_HEADPyObjectob_base;#definePyObject_VAR_HEADPyVarObjectob_base;//宏定义,包括前一个和下一个,用于构建双向链表。(在refchain链表中使用时)#define_PyObject_HEAD_EXTRA\
struct_object*_ob_next;\
struct_object*_ob_prev;typedefstruct_object{
_PyObject_HEAD_EXTRA//用于构建双向链表
Py_ssize_tob_refcnt;//引用计数器
struct_typeobject*ob_type;//数据类型}PyObject;typedefstruct{
PyObjectob_base;///pyobject对象
Py_ssize_tob_size;/*Numberofitemsinvariablepart,也就是说,元素个数*/}PyVarObject;

这两种结构体Pyobject和Pyvarobject是保存其他数据类型公共部分的基石。例如,每种类型的对象在创建Pyobject时都有Pyobject的四部分数据;list/setPyvarobject中的五部分数据是由多个元素组成的对象创建的/tuple。

2.2常见类型结构

通常,当我们创建一个对象时,它本质上是一个相关类型的结构,内部保存值和引用计数器。

float类型

typedefstruct{
PyObject_HEADdoubleob_fval;
}PyFloatObject;

int类型

struct_longobject{
PyObject_VAR_HEAD
digitob_digit[1];
};/*Long(arbitraryprecision)integerobjectinterface*/
typedefstruct_longobjectPyLongObject;/*Revealedinlongintrepr.h*/

str类型

typedefstruct{
PyObject_HEAD
Py_ssize_tlength;/*Numberofcodepointsinthestring*/
Py_hash_thash;/*Hashvalue;-ifnotset*/
struct{
unsignedintinterned:2;/*Charactersize:
-PyUnicode_WCHAR_KIND(0):
*charactertype=wchar_t(16or32bits,dependingonthe
platform)
-PyUnicode__1BYTE__KIND(1):
*charactertype=Py_UCS1(8bits,unsigned)
*allcharactersareintherangeU+0000-U+00FF(latin1)
*ifasciiisset,allcharactersareintherangeU+0000-U+007F
(ASCII),otherwiseatleastonecharacterisintherange
U+0080-U+00FF
-PyUnicode__2BYTE___KIND(2):
*charactertype=Py_UCS2(16bits,unsigned)
*allcharactersareintherangeU+0000-U+FFFF(BMP)
*atleastonecharacterisintherangeU+0100-U+FFFF
-PyUnicode_4BYTE___KIND(4):
*charactertype=Py_UCS4(32bits,unsigned)
*allcharactersareintherangeU+0000-U+10FFFF
*atleastonecharacterisintherangeU+10000-U+10FFFF
*/
unsignedintkind:3;unsignedintcompact:1;unsignedintascii:1;unsignedintready:1;unsignedint:24;
}state;wchar_t*wstr;/*wchar_trepresentation(null-terminated)*/
}PyASCIIObject;typedefstruct{
PyASCIIObject_base;
Py_ssize_tutf8__length;/*Numberofbytesinutf8,excludingthe
*terminating\0.*/
char*utf8;/*UTF-8representation(null-terminated)*/
Py_ssize_twstr_length;/*Numberofcodepointsinwstr,possible
*surrogatescountastwocodepoints.*/
}PyCompactUnicodeObject;typedefstruct{
PyCompactUnicodeObject_base;union{void*any;
Py_UCS1*latin1;
PyUCS2*ucs2;
PyUCS4*ucs4;
}data;/*Canonical,smallest-formUnicodebuffer*/
}PyUnicodeObject;

list类型

typedefstruct{
PyObject_VAR_HEAD
PyObject**ob_item;
Py_ssize_tallocated;
}PyListObject;

tuple类型

typedefstruct{
PyObject_VAR_HEAD
PyObject*ob_item[1];
}PyTupleObject;

dict类型

typedefstruct{
PyObject_HEAD
Py_ssize_tma_used;
PyDictKeysObject*ma_keys;
PyObject**ma_values;
}PyDictObject;

基本上,每个对象内存储的数据都可以通过常见的结构体来理解。

扩展:在结构部分,你应该发现str类型比较繁琐,因为python字符串在处理时需要考虑编码,内部规定(见源代码结构):

如果字符串只包含ascii,则每个字符用一个字节表示,即:latin1字符串包含中文等,则每个字符用两个字节表示:ucs2字符串包含emoji等,则每个字符用四个字节表示:ucs4