内存管理和垃圾回收机制
两个重要的结构体
1 #define _PyObject_HEAD_EXTRA \ 2 struct _object *_ob_next; \ 3 struct _object *_ob_prev; 4 5 #define PyObject_HEAD PyObject ob_base; 6 7 #define PyObject_VAR_HEAD PyVarObject ob_base; 8 9 10 typedef struct _object { 11 _PyObject_HEAD_EXTRA // 用于构造双向链表 12 Py_ssize_t ob_refcnt; // 引用计数器 13 struct _typeobject *ob_type; // 数据类型 14 } PyObject; 15 16 17 typedef struct { 18 PyObject ob_base; // PyObject对象 19 Py_ssize_t ob_size; /* Number of items in variable part,即:元素个数 */ 20 } PyVarObject;
以上源码是Python内存管理中的基石,其中包含了:
- 2个结构体
- PyObject,此结构体中包含3个元素。
- _PyObject_HEAD_EXTRA,用于构造双向链表。
- ob_refcnt,引用计数器。
- *ob_type,数据类型。
- PyVarObject,次结构体中包含4个元素(ob_base中包含3个元素)
- ob_base,PyObject结构体对象,即:包含PyObject结构体中的三个元素。
- ob_size,内部元素个数。
- PyObject,此结构体中包含3个元素。
- 3个宏定义
- PyObject_HEAD,代指PyObject结构体。
- PyVarObject_HEAD,代指PyVarObject对象。
- _PyObject_HEAD_EXTRA,代指前后指针,用于构造双向队列。
Python中所有类型创建对象时,底层都是与PyObject和PyVarObject结构体实现,一般情况下由单个元素组成对象内部会使用PyObject结构体(float)、由多个元素组成的对象内部会使用PyVarObject结构体(str/int/list/dict/tuple/set/自定义类),因为由多个元素组成的话是需要为其维护一个 ob_size(内部元素个数)。
# 第一步:根据float类型所需的内存大小,为其开辟内存。 # 第二步:对新开辟的内存中进行类型和引用的初始化 float类型每次创建对象时都会把对象放一个双向链表中. 引用时,会对其引用计数器+1的动作. # 第三部: 销毁对象 但float内部有缓存机制,所以他的执行流程是这样的 float内部缓存的内存个数已经大于等于100,那么在执行`del val`的语句时,内存中就会直接删除此对象。 未达到100时,那么执行 `del val`语句,不会真的在内存中销毁对象,而是将对象放到一个free_list的单链表中,以便以后的对象使用。 从双向链表中移除
# 在内存创建两个对象,即:引用计数器值都是1 # 两个对象循环引用,导致内存中对象的应用+1,即:引用计数器值都是2 # 删除变量,并将引用计数器-1。 # 关闭垃圾回收机制,因为python的垃圾回收机制是:引用计数器、标记清除、分代回收 配合已解决循环引用的问题,关闭他便于之后查询内存中未被释放对象。 # 至此,由于循环引用导致内存中创建的obj1和obj2两个对象引用计数器不为0,无法被垃圾回收机制回收。 # 所以,内存中Foo类的对象就还显示有2个。
循环引用的问题会引发内存中的对象一直无法释放,从而内存逐渐增大,最终导致内存泄露
为了解决循环引用的问题,Python又在引用计数器的基础上引入了标记清除和分代回收的机制。
Python为了解决循环引用,针对 lists, tuples, instances, classes, dictionaries, and functions 类型,每创建一个对象都会将对象放到一个双向链表中,每个对象中都有 _ob_next 和 _ob_prev 指针,用于挂靠到链表中。
随着对象的创建,该双向链表上的对象会越来越多。
-
当对象个数超过 700个 时,Python解释器就会进行垃圾回收。
-
当代码中主动执行 gc.collect() 命令时,Python解释器就会进行垃圾回收。
Python解释器在垃圾回收时,会遍历链表中的每个对象,如果存在循环引用,就将存在循环引用的对象的引用计数器 -1,同时Python解释器也会将计数器等于0(可回收)和不等于0(不可回收)的一分为二,把计数器等于0的所有对象进行回收,把计数器不为0的对象放到另外一个双向链表表(即:分代回收的下一代)。
总结
- 开启一个新的对象会存放到双端链表中
- 通过引用计数来决定是不是垃圾,但是会有循环引用的问题
- 为了解决循环引用,使用了标记清除,标记清除就是循环引用的内容引用计数自建1
- 为了解决多次扫描一个双端链表,使用了分代回收,一共3个代 0,1,2
- 当0代的总长度>=700是扫描一下0代,当0代扫描10次后扫描一次1代
- 当1代扫描10次后扫描一次2代