一句话概括:引用计数为主,标记清除和分代回收为辅,另外还有缓存机制;
基于C语言源码学习python(3.8.2)的垃圾回收机制;
在学习python垃圾回收机制的C语言源码之前,需要知道一种数据结构,环状双向链表(refchain)
。
实现一个环形的双向链表,链表的每个节点都保存三个信息,当前节点的值value,前一个节点的指针prev,后一个节点的指针next。因为是环形的,所以最后一个节点的next指向第一个节点,而第一个节点的prev指向最后一个节点
如果只存在一个节点,那么这个节点的prev和next都会指向这个节点本身。
在python中创建的任何对象都会被放在这个环状链表中。不同数据类型放到这个链表中的时候里面的内容会有所不同。
name = "张三"
age = 22
info = ["北京市", "13812345678", "程序员"]
new_info = info # 指向同一个对象,该对象引用次数+1
在双向链表中,保存的信息包括:上一个对象的指针
, 下一个对象的指针
, 数据类型
, 引用次数
;不同数据类型,存储的信息不完全相同; 比如,数值类型的对象,还可能将数据的值存储在环状链表中,如果是列表/元组,还需要存储其各项元素以及长度;
对应的C语言源码:
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
// 宏定义,包含 上一个、下一个,用于构造双向链表。
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object { // 这个结构体相当于存了4个属性
_PyObject_HEAD_EXTRA // 用于构造双向链表,这里相当于上面定义的两行6和7
Py_ssize_t ob_refcnt; // 引用计数器
struct _typeobject *ob_type; // 数据类型
} PyObject;
typedef struct {
PyObject ob_base; // PyObject对象(继承上一个结构体,包含上一个结构体的4个属性)
Py_ssize_t ob_size; // Number of items in variable part,即 元素个数
} PyVarObject;
也就是说,在C语言源码中,每个对象都有的属性,包括:上一个、下一个、引用个数、数据类型,保存在PyObject
这个结构体中;可以简单理解为这个是基类,继承这个基类之后,就会拥有这些属性。
类似于列表、元组等由多个元素组成的数据类型,在PyObject
这个基类拥有的4个属性的基础上,再加上ob_size
(元素个数)这个属性,通过“继承”的方式来实现(这里说的“继承”并非真正意义上的继承,举个直白点的例子,一堆易拉罐,可以用胶带将每3个缠一起,也可以将缠好的3个作为整体,再往这3个外面粘贴另一个)。
python中常见的数据类型C语言源码:
// float 类型
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
// int 类型
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
// Long (arbitrary precision) integer object interface
typedef struct _longobject PyLongObject; // Revealed in longintrepr.h
// list 类型
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
// tuple 类型
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
// dict 类型
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
举个例子:现在在python里面创建一个浮点类型数值,如:a = 1.2
,就会使用float类型对应的结构体,内部创建的属性有:
_ob_next 指向refchain中的下一个对象
_ob_prev 指向refchain中的上一个对象
ob_refcnt = 1
ob_type = float
ob_fval = 1.2
当python程序运行时,会根据数据类型的不同,找到对应的C语言源码中的结构体,再根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain双向链表中;
在C语言源码中,有两个关键的结构体,PyObject
和PyVarObject
,创建一个Python的对象时,如a = 3
,引用计数器ob_refcnt
默认值为1,当有其他变量引用该对象的时候,比如b = a
,引用计数器ob_refcnt
就会增加1,此时refcnt=12
;如果删除了变量的引用,或者说断开引用,比如使用del b
,这个时候引用计数器refcnt
就会减少1。
a = 3 # refcnt = 1
b = a # refcnt = 2
del b # refcnt = 1
del a # refcnt = 0
当一个对象的引用计数器为0时,就表示没有变量要引用这个对象,也就是说这个对象就是没用的对象,可以视为垃圾,就要进行垃圾回收,也就是说要将创建对象时申请的内存归还给操作系统(这里暂时先不说缓存机制);
循环引用
比如下面的代码:
li_1 = [1, 2, 3] # refchain中创建一个列表对象,引用计数器refcnt=1
li_2 = [4, 5, 6] # refchain中创建一个列表对象,引用计数器refcnt=1
li_1.append(li_2) # li_1中引用了[4, 5, 6], 于是引用计数器+1,此时refcnt=2
li_2.append(li_1) # li_2中引用了[1, 2, 3], 于是引用计数器+1,此时refcnt=2
del li_1 # 删除1个,refcnt-1 = 1
del li_2 # 删除1个,refcnt-1 = 1
# 此时虽然删除了两个对象,但是由于相互引用,导致无法释放内存空间
目的:为了解决python引用计数器循环引用导致内存泄露的缺陷;
实现:在python的底层,再多维护一个链表,链表中存放可能会被循环引用的对象,如list
类型、dict
类型等;
在特定情况下,扫描可能存在循环引用对象的链表中的每个元素,如果发现循环引用,会让双方的引用计数器都减去1,如果减完之后引用计数器为0,则进行回收;
目的:弥补引用计数器的缺陷;
实现:将可能存在循环引用的对象维护成3个环状双向链表;分别为0代、1代、2代;
机制:
在创建对象时,同时将对象添加到refchain和0代中;在0代中的对象达到700个时,触发扫描,此时对0代中的对象进行扫描,如果发现循环引用,将其refchain中的引用计数器减去1,如果减完之后引用计数器为0,则将其回收,再将剩余没有被回收的对象转移到1代中去;
在python底层中维护了一个refchain的环状双向链表这个链表中存储了程序创建的所有对象,而且保存了每个对象的引用计数器ob_refcnt
,当对象被引用时,引用计数器+1,断开引用时,引用计数器-1,当引用计数器变为0时,对象所占用的内存空间会被释放,refchain中也会移除该对象。
但是对于python中的list、tuple、dict等,可能会存在循环引用的情况,又引入了标记清除和分代回收机制;
也就是说在python底层,维护了至少4个链表:
python源码内部上述流程中提出了优化机制:缓存;
为了避免重复创建和销毁一些常见的对象,python底层还维护了一个内存池,用于对小块内存的申请与释放的管理,如果创建多个小内存对象,频繁调用python的内存分配器(PyMalloc)来分配内存,会大大降低程序性能,所以python底层实现了内存池机制;
内存池机制就是预先在内存中申请大小相等的内存块用于备用,在申请内存时,如果申请的内存没有超过256kb
,就会优先从内存池中分配内存,这样做会减少内存碎片,提升内存利用率;当所需内存大于256kb
的时候,才会执行malloc
来重新分配内存。
另外,在释放内存的时候也是一样,当变量引用数量为0时,将从内存池分配的空间归还,而不是直接释放,这样做也可以减少频繁释放内存导致的性能下降。
以上为参考其他人的观点与个人的见解,如有不当之处,欢迎批评指正,共同进步!!!