内存管理和垃圾回收机制

结构

Python解释器由c语言开发完成,py中所有的操作最终都由底层的c语言来实现并完成,所以想要了解底层内存管理需要结合python源码来进行解释。

两个重要的结构体

include/object.h

 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,内部元素个数。
  • 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类型所需的内存大小,为其开辟内存。
# 第二步:对新开辟的内存中进行类型和引用的初始化
    float类型每次创建对象时都会把对象放一个双向链表中.
    引用时,会对其引用计数器+1的动作.
# 第三部: 销毁对象
    但float内部有缓存机制,所以他的执行流程是这样的
        float内部缓存的内存个数已经大于等于100,那么在执行`del val`的语句时,内存中就会直接删除此对象。
        未达到100时,那么执行 `del val`语句,不会真的在内存中销毁对象,而是将对象放到一个free_list的单链表中,以便以后的对象使用。
    从双向链表中移除

 

垃圾回收机制

Python的垃圾回收机制是以:引用计数器为主,标记清除和分代回收为辅。

1.引用计数器

每个对象内部都维护了一个值,该值记录这此对象被引用的次数,如果次数为0,则Python垃圾回收机制会自动清除此对象。下图是Python源码中引用计数器存储的代码。

2.循环引用

通过引用计数器的方式基本上可以完成Python的垃圾回收,但它还是具有明显的缺陷,即:“循环引用”

 

循环引用

# 在内存创建两个对象,即:引用计数器值都是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. 开启一个新的对象会存放到双端链表中
  2. 通过引用计数来决定是不是垃圾,但是会有循环引用的问题
  3. 为了解决循环引用,使用了标记清除,标记清除就是循环引用的内容引用计数自建1
  4. 为了解决多次扫描一个双端链表,使用了分代回收,一共3个代 0,1,2
  5. 当0代的总长度>=700是扫描一下0代,当0代扫描10次后扫描一次1代
  6. 当1代扫描10次后扫描一次2代

 

 

 

 

 

 

 

 

posted @ 2019-12-18 16:01  驰念  阅读(378)  评论(0编辑  收藏  举报