Python–垃圾回收机制
引用计数器为主
标记清除和分代回收为辅
+缓存机制
基于C语言源码 底层,**让你真正了解垃圾回收机制的实现
- 引用计数器
- 标记清除
- 分代回收
- 缓存机制
- Python的C源码(3.8.2版本)
一.引用计数器
1.环状双向链表
在python程序中创建
内部会创建一些数据[上一个对象,下一个对象,类型,引用个数]
name = "LQ6H"
内部会创建一些数据[上一个对象,下一个对象,类型,引用个数,value=20]
age = 20
内部会创建一些数据[上一个对象,下一个对象,类型,引用个数,items=元素,元素个数]
name = ["John","Smith"]
// Include/Object.h
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
// 宏定义,包含上一个,下一个,用于构造双向链表用(放到refchain链表中时使用)
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA // 用于构造双向链表
Py_ssize_t ob_refcnt; // 引用计数器
struct _typeobject *ob_type; // 数据类型
} PyObject;
typedef struct {
PyObject ob_base; // PyObject对象
Py_ssize_t ob_size; /* Number of items in variable part 即:元素个数 */
} PyVarObject;
在C源码中如何体现每个对象中都有的相同的值:PyObject结构体(4个值)
有多个元素组成的对象:PyObject结构体(4个值)+ob_size
2.类型封装结构体
- Float类型
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
a = 3.14
内部会创建:
_ob_next = refchain中的上一个对象
_ob_prev = refchain中的下一个对象
refcnt = 1
ob_type = float
ob_fval = 3.14
**
- Int类型
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
**
- List类型
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
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_value;
} PyDictObject;
3.引用计数器
v1 = 3.14
v2 = 999
v3 = (1,2,3)
当python程序运行时,会根据数据类型的不同找到对应的结构体,根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain双向链表中。
在C源码中有两个关键的结构体:PyObject,PyVarObject。
每个对象中有ob_refcnt就是引用计数器,值默认为1,当有其他变量引用对象时,引用计数器就会发生变化。
- 引用
a = 999
b = a
- 删除引用
a = 999
b = a
del b # b变量删除:b对应对象引用计数器-1
del a # a变量删除:a对应对象引用计数器-1
# 当一个对象的引用计数器为0时,意味着没有人再使用这个对象,这个对象就是垃圾,垃圾回收
# 回收:1.对象从refchain链表移除;2.将对象销毁,内存归还
4.循环引用的问题
循环引用&交叉感染
v1 = [1,2,3] # refchain中创建一个列表对象,由于v1=对象,所以列表对象用计数器为1
v2 = [4,5,6] # refchain中再创建一个列表对象,由于v2=对象,所以列表对象用计数器为1
v1.append(v2) # 把v2追加到v1中,则v2对应的[4,5,6]对象的引用计数器加1,最终为2
v2.append(v1) # 把v1追加到v2中,则v1对应的[1,2,3]对象的引用计数器加1,最终为2
del v1 # 引用计数器减1
del v2 # 引用计数器减1
二.标记清除
目的:为了解决引用计数器循环引用的不足
**实现:**在python的底层再维护一个链表,链表中专门放那些可能存在循环引用的对象(list/tuple/dict/set)
在python内部,某种情况下触发,回去扫描可能存在循环引用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器各自-1;如果是0则垃圾回收。
问题:
- 什么时候扫描?
- 可能存在循环引用的链表扫描代价大,每次扫描耗时长
三.分代回收
将可能存在循环引用的对象维护成3个链表:
- 0代:0代中对象个数达到700个扫描一次
- 1代:0代扫描10次,1代扫描一次
- 2代:1代扫描10次,2代扫描一次
**小结:**
在python中维护了一个**refchain**的双向环状链表,这个链表中存储程序创建的所有对象,每种类型的对象中都有一个**ob_refcnt引用计数器的值,**引用个数+1,-1,最后当引用计数器变为0时会进行垃圾回收(对象销毁,refchain中移除)
但是,在python中对于那些可以有多个元素组成的对象可能会存在循环引用的问题,为了解决这个问题python又引入了**标记清除和分代回收,**在其内部有4个链表:
- refchain
- 0代,700个
- 1代,0代10次
- 2代,1代10次
在源码内部当达到各自的阈值时,就会触发扫描链表进行标记清除的操作(有循环则各自-1)
但是,源码内部在上述的流程中提出了优化机制
四.Python缓存
1.池(int)
为了避免重复创建和销毁一些常见对象,维护池
# 启动解释器时,python内部帮我们创建:-5,-4,......257
v1 = 7 # 内部不会开辟内存,直接去池中获取
v2 = 9 # 内部不会开辟内存,直接去池中获取
v3 = 9 # 内部不会开辟内存,直接去池中获取
print(id(v2),id(v3))
2.free_list
当一个对象的引用计数器为0时,按理说应该回收,内部不会直接回收,而是将对象添加到free_list链表中当缓存。以后再去创建对象时,不再重新开辟内存,而是直接使用free_list。
v1 = 3.14 # 开辟内存,内部存储结构体定义几个值,并存到refchain中
del v1 # refchain中移除,将对象添加到free_list中(假设最多80个),free_list满了则销毁
v9 = 999.99 # 不会重新开辟内存,去free_list中获取对象,对象内部数据初始化,再放到refchain中
参考链接2
五.源码分析
链接**:**