2.3 内存管理和垃圾回收
在python中说到内存管理和垃圾回收,经典的一句话就是:引用计数器为主,标记清除和分代回收为辅+缓存机制。
2.3.1 引用计数器
在python中程序所创建的对象都会存贮在一个refchain(环状双向链表)中,是创建的任何数据对象实例都存储其中。在底层代码C语言的实现中,我们可以认为在创建对象的时候在内存空间中创建了一个结构体,为了实现环状双向链表的功能,所以其结构体构成需要有相同的元素,主要的四个相同的元素为:上一个对象的指针,下一个对象的指针,该对象的数据类型,引用个数(变量对该结构体的内存的映射个数)。当然不同的数据类型所存储的结构体的不同点,就是数据值的不同等等。
可以看一下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{
_PyObject_HEAD_EXTRA //用于构建双向链表
Py_ssize_t ob_refcnt; // 引用计数器(该对象内存空间的引用个数)
struct _typeobject *ob_type; // 数据类型
}PyObject;
typedef struct{
PyObject ob_base; //PyObject对象
Py_ssize_t ob_size; //元素个数
}PyVarObject;
也就是说在C源码中如何体现每个对象中都有的相同的值:PyObject
结构体(4个值)
不同点:有多个元素组成的对象(如列表,字典等):PyObject
结构体(4个值) + ob_size
(元素个数)
// float类型的结构体定义方法
typedef struct{
PyObject_HEAD
double ob_fval;
}PyFloatObject;
// list类型的结构体定义方式
typedef struct{
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
}PyListObject;
如同上述举例看出:float
类型的对象使用的是PyObject
结构体,再加上一个ob_fval
用来保存具体的数值。而list
类型使用的是PyObject_VAR_HEAD
结构体,定义其变量的初始状态。ob_item
是指向PyObject
类型的指针的指针。首先说明一下,list是一个容器,里面可以容纳很多PyObject
类型的对象,ob_item
指向一块连续空间,这块空间装载了n个指针,每一个指针指向一个PyObject
对象。allocated
表明当前为ob_item
分配了多少空间。由此可以看出list
是一个顺序表。
综上所述,当python程序运行时,会根据数据类型的不同找到其对应的结构体,根据结构体中的字段来进行创建相关的数据,然后将对象添加到refchain
双线链表中。每个对象中有 ob_refcnt
就是引用计数器,值默认为 1 ,当有其他变量引用对象时,引用计数器就会发生变化。
a = 3.14 # 创建了一个float对象 其计数引用器默认为1
b = a # 现在b引用了该对象,该对象的引用计数器为2
del b # 现在删除了b变量,那么其计数引用器默认为1
del a # 现在删除了a变量,那么其计数引用器默认为0 ,所以释放内存空间
当一个对象的引用计数器为0时,意味着没有人再使用这个对象了,这个对象就是垃圾,垃圾回收。其所进行的操作是:
-
1.对象从
refchain
链表移除; -
2.(可以简单认为)将对象销毁,内存归还。
如果只使用引用计数器方法,那就会出现一个大问题,如果变量之间互相引用,形成循环引用,导致交叉感染,如果在把变量全部清除掉的话, 由于引用计数器不为0,所以内存空间不会释放,也就是说没有变量去应用该对象的内存空间,也就是形成了脏数据。
v1 = [1, 2, 3] # refchain中创建一个列表对象,由于v1=对象,所以列表对象的应用计数器为1
v2 = [4, 5, 6] # refchain中创建一个列表对象,由于v2=对象,所以列表对象的应用计数器为1
v1.append(v2) # 把v2追加到v1中去,因为引用了v2的对象所以该对象的引用计数器加1,最终为2
v2.append(v1) # 把v1追加到v2中去,因为引用了v1的对象所以该对象的引用计数器加1,最终为2
del v1 # 引用计数器-1
del v2 # 引用计数器-1
# 消除了所有变量,但是由于两个对象的应用计数器没有为0,所以对象不能被销毁,内存空间没有变量引用,所以形成脏数据
2.3.2 标记清除
为了解决由于引用计数器的不足,从而引起的循环引用的问题,使用了标记清除这个概念。
实现:在python的底层再维护一个链表,链表中专门放那些可能存在循环引用的对象(list/tuple/dict/set
)。基于两个链表进行维护,如下图所示。
在Python内部某种情况下触发,回去扫描可能存在循环应用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器 -1 ;如果是0则垃圾回收。
但是标记清除技术的引用会带来两个问题,一个问题是什么时候需要对可能存在循环引用的链表进行扫描;还有一个问题是,由于扫描循环引用列表需要对每一个元素都进行扫描,所以每次扫描的代价很大,所需要的持续时间很久。
2.3.3 分代回收
为了解决上述标记清除提出的两个问题,python又引用了分代回收机制,简单来说就是把原来的一个可能存在循环应用的链表,拆成了三个链表,如下图所示:
将可能存在循环应用的对象维护成3个链表:
- 0代:0代中对象个数达到700个扫描一次
- 1代:0代扫描10次,则1代扫描一次
- 2代:1代扫描10次,则2代扫描一次
也就是说,首先把列表等可能引起循环引用的对象元素全部放入在0代链表中,只有当0代链表中的对象个数达到700个,那么就会对0代中所有的元素进行扫描,如果出现循环引用那就使该对象的引用计数器减1,如果引用计数器为0,那就进行垃圾回收。然后把0代中剩余的对象元素放入一代中,并且清除0代中的对象元素,只有当0代这种操作执行10次,那么1代链表中的元素再一次进行扫描,只有当1代中的元素,扫描了10次,那么就会把1代中的对象元素升入2代中,并且清除1代中的对象元素,以此往复。
2.3.4 缓存机制
在python中对上述的流程又提出了优化机制,所以引入缓存机制。在python中缓存可以分为两个部分:池和free_list
。
池:通常是int
和字符串使用
python中为为了避免重复创建和销毁一些常见对象,就创建了维护池(内部创建好了一些对象,需要变量引用的时候就直接去拿,不需要重复的创建和销毁)。
# 启动解释器时,Python内部帮我们创建:-5、-4、..... 257
v1 = 7 # 内部不会开辟内存,直接去池中获取
v2 = 9 # 内部不会开辟内存,直接去池中获取
v3 = 9 # 内部不会开辟内存,直接去池中获取
print(id(v2),id(v3)) # 会发现这两个内存地址是相同的
free_list
:通常是float
/list
/tuple
/dict
等使用
当一个对象的引用计数器为0时,按理说应该回收,但是python为了优化,内部不会直接回收,而是将对象添加到 free_list 链表中当缓存。以后再去创建对象时,不再重新开辟内存,而是直接使用free_list。
v1 = 3.14 # 开辟内存,内部存储结构体中定义那几个值,并存到refchain中。
del v1 # refchain中移除,将对象添加到 free_list 中,如果free_list满了则销毁。
v9 = 999.99 # 不会重新开辟内存,去free_list中获取对象,对象内部数据初始化,再放到refchain中。
2.3.5 C语言的底层源码实现
以float类型举例如下:
// float类型的结构体定义方法
typedef struct{
PyObject_HEAD
double ob_fval;
}PyFloatObject;
创建float对象时,需要运行python语句:
val = 3.14
当按照上述方式创建一个Float类型对象时,python源码内部会先后执行如下代码
/* Special free list
free_list is a singly-linked list of available PyFloatObjects, linked
via abuse of their ob_type members.
*/
static PyFloatObject *free_list = NULL;
static int numfree = 0;
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op = free_list;
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op);
numfree--;
} else {
// 第一步:根据float类型大小,为float对象开辟内存。
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
if (!op)
return PyErr_NoMemory();
}
// 第二步:在为float对象开辟的内存中进行初始化。
/* Inline PyObject_New */
(void)PyObject_INIT(op, &PyFloat_Type);
// 第三步:将值赋值到float对象开辟的内存中。
op->ob_fval = fval;
// 第四步:返回已经创建的float对象的内存地址(引用/指针)
return (PyObject *) op;
}
第一步:根据float类型所需的内存大小,为其开辟内存
static PyMemAllocatorEx _PyObject = {
#ifdef PYMALLOC_DEBUG
&_PyMem_Debug.obj, PYDBG_FUNCS
#else
NULL, PYOBJ_FUNCS
#endif
};
void *PyObject_Malloc(size_t size)
{
/* see PyMem_RawMalloc() */
if (size > (size_t)PY_SSIZE_T_MAX)
return NULL;
// 开辟内存
return _PyObject.malloc(_PyObject.ctx, size);
}
第二步:对新开辟的内存中进行类型和引用的初始化
#define PyObject_INIT(op, typeobj) \
( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )
static PyObject refchain = {&refchain, &refchain};
void _Py_AddToAllObjects(PyObject *op, int force)
{
if (force || op->_ob_prev == NULL) {
op->_ob_next = refchain._ob_next;
op->_ob_prev = &refchain;
refchain._ob_next->_ob_prev = op;
refchain._ob_next = op;
}
}
void _Py_NewReference(PyObject *op)
{
_Py_INC_REFTOTAL;
// 对新开辟的内存中的的引用计数器初始化为1。
op->ob_refcnt = 1;
// 将新开辟的内存的指针添加到一个双向链表refchain中。
_Py_AddToAllObjects(op, 1);
_Py_INC_TPALLOCS(op);
}
float对象引用时,在python代码中
val = 7.8
data = val
在源码中实现:
//在给对象创建新引用时,会对其引用计数器+1的动作
#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++)
销毁对象时,python代码
val = 3.14
# 主动删除对象
del val
在源码中实现:
#define Py_DECREF(op) \
do { \
PyObject *_py_decref_tmp = (PyObject *)(op); \
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \
--(_py_decref_tmp)->ob_refcnt != 0) \
_Py_CHECK_REFCNT(_py_decref_tmp) \
else \
_Py_Dealloc(_py_decref_tmp); \
} while (0)
void
_Py_Dealloc(PyObject *op)
{
// 第一步:调用float类型的tp_dealloc,进行内存的销毁
destructor dealloc = Py_TYPE(op)->tp_dealloc;
// 第二步:在refchain双向链表中移除
_Py_ForgetReference(op);
(*dealloc)(op);
}
Objects/object.c
第一步,调用float类型的tp_dealloc
进行内存的销毁。
按理此过程说应该直接将对象内存销毁,但float内部有缓存机制,所以他的执行流程是这样的:
-
float内部缓存的内存个数已经大于等于100,那么在执行
del val
的语句时,内存中就会直接删除此对象。 -
未达到100时,那么执行
del val
语句,不会真的在内存中销毁对象,而是将对象放到一个free_list的单链表中,以便以后的对象使用。#ifndef PyFloat_MAXFREELIST #define PyFloat_MAXFREELIST 100 #endif static int numfree = 0; static PyFloatObject *free_list = NULL; PyTypeObject PyFloat_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "float", sizeof(PyFloatObject), 0, // tp_dealloc表示执行float_dealloc方法 (destructor)float_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ ... }; static void float_dealloc(PyFloatObject *op) { // 检测是否是float类型 if (PyFloat_CheckExact(op)) { // 检测缓冲池个数是否大于100个 if (numfree >= PyFloat_MAXFREELIST) { // 如果大于100个,则在内存中销毁对象 PyObject_FREE(op); return; } // 否则,缓冲池个数+1 // 并将要销毁的数据加入到free_list的单项链表中,以便以后创建float类型使用。 numfree++; Py_TYPE(op) = (struct _typeobject *)free_list; free_list = op; } else Py_TYPE(op)->tp_free((PyObject *)op); }
PyObject_FREE(op); return; } // 否则,缓冲池个数+1 // 并将要销毁的数据加入到free_list的单项链表中,以便以后创建float类型使用。 numfree++; Py_TYPE(op) = (struct _typeobject *)free_list; free_list = op; } else Py_TYPE(op)->tp_free((PyObject *)op);
}