零 概述
首先说明一下,源码是基于CPython
Python垃圾回收机制是以引用计数为主,标记清除和分代回收为辅,再加缓存机制,以提升Python性能。
一 引用计数
1 环形双向链表 refchain
在 Python 中创建的任何对象,都会加入到一个双向循环链表 refchain 中。对象节点的大致结构如下:
下一个对象
上一个对象
引用计数
对象类型
CPython源码:
// cpython/Include/object.h
// 上一个对象 和 上一个对象
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
// 对象类型
typedef struct _typeobject PyTypeObject;
// 对象节点
typedef struct _object {
_PyObject_HEAD_EXTRA // 上一个对象 和 上一个对象
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 对象类型
} PyObject;
// 集合对象类型
typedef struct {
PyObject ob_base; // 对象类型
Py_ssize_t ob_size; // 元素个数
} PyVarObject;
2 对象类型结构体
对于不同的数据类型,python封装了不同的结构体,CPython源码:
// 对象节点 cpython/Include/object.h
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
// 浮点数对象 cpython/Include/floatobject.h
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
// 整数 cpython/Include/longintrepr.h
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
// 整数对象 cpython/Include/longobject.h
typedef struct _longobject PyLongObject;
// 元组对象 cpython/Include/cpython/tupleobject.h
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
// 列表对象 cpython/Include/cpython/listobject.h
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
// 字典对象 cpython/Include/cpython/dictobject.h
typedef struct {
PyPyObject_HEAD
Py_ssize_t ma_used;
uint64_t ma_version_tag;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
// 集合对象 cpython/Include/setobject.h
typedef struct {
PyObject_HEAD
Py_ssize_t fill;
Py_ssize_t used;
Py_ssize_t mask;
setentry *table;
Py_hash_t hash;
Py_ssize_t finger;
setentry smalltable[PySet_MINSIZE];
PyObject *weakreflist;
} PySetObject;
每种类型都含有相同的部分:PyPyObject,集合类型 PyVarObject 多一个 ob_size,其他的数据每种类型有各自的定义。
当在python中定义一个对象时,Python解释器会在内存中创建一个结构体。例如:
pi=3.14
python 解释器中会创建一个结构体:
struct PyFloatObject floatObject = {
._ob_next = null, // 指向环形链表的下一个节点
._ob_prev = null, // 指向环形链表的前一个节点
.ob_refcnt = 1, // 引用计数
.ob_type = float, // 数据类型
.ob_fval = 3.14, // 记录值
};
3 引用计数
Python 程序运行时,根据不同的数据类型 创建不同的结构体,然后加入到双向循环链表中。每中类型的结构体含相同部分 PyObject,其中 ob_refcnt 表示引用计数,默认为1,当其他变量引用对象时,引用计数会发生变化。当计数器为0时,被系统回收——从双向链表中移除,然后释放结构体。
- 增加引用
a = 1 // 默认引用计数为1
b = a // 引用计数加1,变为2
- 删除引用
a = 1 // 默认引用计数为1
b = a // 引用计数加1,变为2
del b // 引用计数减1,变为1
del a // 引用计数减1,变为0,被系统回收
二 标记清除
看下面这个例子:
l1 = [11, 12, 13] // l1默认引用计数为1
l2 = [21, 22, 23] // l1默认引用计数为1
l1.append(l2) // l2引用计数为2
l2.append(l1) // l1引用计数为2
del l1 // l1引用计数为1
del l2 // l2引用计数为1
当删除变量l1和l2时引用计数没有变为0,但这两个变量不再被使用,变量没有被回收,就会出现内存泄漏。因此当出现循环引用时,单纯使用引用计数,会出现内存泄漏,为了解决这一问题,Python中引入了标记清除。
维护另一个链表,当创建集合对象时(元组,列表,字典,集合),同时加入到此链表中。Python解释器会不定时的扫描这个链表,检测是否存在循环引用,存在则将双方的引用计数减1。
三 分代回收
标记清除有两个问题:
- 何时扫描
- 扫描的性能
分代回收回收就是为了解决这个问题。
将可能存在循环引用的集合对象的链表分为3个:
- 0代:对象达到700时扫描一次
- 1代:0代扫描10次,1代扫描1次
- 2代:1代扫描10,2代扫描1次
那么标记清楚的过程就变成了:
- 当创建集合对象时,加入双向循环量表的同时,也加入到0代中;
- 当0代链表中对象数量达到700时,扫描一次0代,如果有循环引用则将双方引用计数都减1,引用计数变为0时则回收,不为0则将对象从0代链表移入1代链表;
- 当0代链表扫描10次时,扫描1代链表,如果有循环引用则将双方引用计数都减1,引用计数变为0时则回收,不为0则将对象从1代链表移入2代链表;
- 当1代链表扫描10次时,扫描2代链表,如果有循环引用则将双方引用计数都减1,引用计数变为0时则回收。
四 缓存
1 内存池(int)
为了避免重复创建和消耗一些常见对象,维护了一个常见对象的对象池。
# 在启动解释器时,会创建 -5 到 256 的整数
# 不会开辟内存,直接从内存池中取
v1 = 1
v2 = 1
# 打印出来的值是相同的
print(id(v1), id(v2))
# 超过 256 的数值,会重新开辟内存
v3 = 257
v4 = 257
# 打印出来的值是不相同的
print(id(v3), id(v4))
2 free list(float/char/list/dict/tuple)
当引用计数为0时,对象会被回收,但对于常用变量,引用计数为0时不销毁,而是将其加入到 free list 中;当再创建对象时,不开辟内存空间,而是直接使用 free list 中已有的对象。
pi = 3.14 # 引用计数为1
print(id(pi))
del pi # 引用计数为0,只从refchain中移除,加入到 free list 中,不释放内存
new_pi = 3.14 # 从 free list 取出元素,不重新分配内存
print(id(new_pi)) # 打印出来的值跟 pi 一样
free list 的长度限制:
- float: 100
- char: 所有ascii
- list:80
- dict:80
- tuple:20,[[空元组], [长度为1元组], …, [长度为19元组]],每个元素元组最长为2000
3 字符串驻留机制(str)
针对只含有字母、数字、下划线的字符串,如果内存中已存在则不会重新创建,而是使用原来的地址里
str1 = "abc"
str2 = "abc"
print(id(str1),id(str2)) # 打印出来的值相同
五 小结
在 Python 解释器中维护了一个双向循环链表,存储程序中创建的所有对象,每种类型的对象都有一个引用计数,当增加引用时,引用计数加1,删除时引用计数减1,当引用计数为0后,则回收(双向循环量表中移除,并消耗对象);
当集合对象出现循环引用时,引用计数会出错,因此 Python 中引入了标记清除,扫描集合对象是否有现循环引用,有则将双方的引用计数减1。
为了解决标记清除中扫面的时机和性能问题,加入了分代回收机制,维护3个三代集合对象链表,当达到条件才,才扫描对应代的链表。
为了避免重复的开辟和释放内存,python 中引入了缓存机制。
参考: