Python内存管理与垃圾回收

Python垃圾回收

视频链接:https://www.bilibili.com/video/BV1F54114761

参考博客:https://pythonav.com/wiki/detail/6/88/

alt

1. 引用计数器

1.1 环状的双向链表

alt

在python程序中创建的任何对象都会放在refchain链表中。

name="数轴级"
age=18
hobby=["唱","篮球","rap"]
//这些创建的对象都会被放在环形双向链表中

内部会创建一些数据【上一个对象,下一个对象,类型,引用个数】

因为存储在环形链表中,需要指向上一个对象,下一个对象。每一个被创建的对象都有这四个值。

name="数轴级"
new =name //两个对象此时会指向同一个内存 此时的引用个数为2

对于不同的数据类型也会创建不同的数据

内部会创建一些数据【上一个对象,下一个对象,类型,引用个数,val=18】
age=18
【上一个对象,下一个对象,类型,引用个数,item=hobby=["唱","篮球","rap"],元素的个数】
hobby=["唱","篮球","rap"]

refchain底层 C语言源码

alt

9-13行存储了四个固定的数据,【上一个对象,下一个对象,类型,引用个数】使用Pyobject(4个值)结构体体现。

15-18行 对于一个对象有多个值的时候会存储【Pyobject(4个值),元素的个数】使用pyVarObject结构体。

对于

1.2 类型封装的结构体

data=3.14
//内部会创建四个固定值,Pyobject(4个值)
  ob_next=refchain中上一个对象
  ob_pre=refchain中下一个对象
  ob_refcnt=1  引用个数
  ob_type= float 数据类型
  ob_fval=3.14  值
  

alt

1.3 引用计数器

v1=2
v2=3.14
v3="自凤凰城"
v4=[1,2,3]
  1. 当python程序运行时,会根据创建类型的不同找到相应的结构体,根据结构体中的字段进行创建不同的相关数据,然后将对象添加到refchain双向链表中。

  2. 在C语言源码有2 个重要的结构体, Pyobject【用于创建四个公共的值上一个对象,下一个对象,类型,引用个数 】,对于有多个对象的值使用PyVarObject【Pyobject,元素个数】

  3. 每个对象中都会有个ob_refcnt就是引用计数器,默认值为1,当有其他变量引用对象是,引用计数器就会发生变化。

    • 引用

      a=999
      b=a #此时a的引用计数器会+1
      
  • 删除引用

    a=999
    b=a
    del b #b变量删除此时b对应的应用计数器会-1
    del a  #a变量删除此时a对应的应用计数器会-1
    
  • 当一个对象的引用计数器为0时,意味着没人在使用这个对象了,这个对象就是垃圾,需要进行垃圾回收。

  • 回收 1、对象从rechain链表中移除 2、将对象销毁内存归还

    引用图示

alt

删除图示

将对象从双向环形链表中删除

alt

1.4 循环引用问题

循环引用与交叉感染

当引用计数器为0时,对象就要进行垃圾回收,但也存在着bug.

第三行代码,v1中使用了v2,导致v2的引用计数器+1,最终为2。

第四行代码,v2中使用了v1,导致v1的引用计数器+1,最终为2。

v1=[11,22,33]  #refchain中创建一 个列表对象, 由于v1=对 象,所以列表引对象用计数器为1.
v2=[44,55,66]  #refchain中创建一 个列表对象, 由于v1=对 象,所以列表引对象用计数器为1.
v1.appenad(v2) #把v2追加到v1中,则v2对 应的[44, 55 , 66]对象的引用计数器加1,最终为2.
v2.appenad(v1) #把v1追加到v1中,则v1对应的[11, 22,33]对象的引用计数器加1,最终为2.

此时对象的refchain为下图

alt

当进行删除对象时,由于上面都进行了引用引用计数器都为2

del v1  #此时v1的引用计数器为1 
del v2  #此时v2的引用计数器为1 

由于我们进行了删除v1 2对象操作 但是此时的引用计数器都不为0,并不会被当做垃圾进行回收。此时程序但此时并不会去使用v1 v2 导致占用内存,如果程序中存在大量的这种代码,会有爆栈危险。为了解决循环引用的问题python引入了标记清除

2. 标记清除

目的:为了解决引用计数器循环引用的不足。

实现:在python的底层在维护一个链表,链表中专门寸法那些可能存在循环引用的对象【列表/元祖/字典/集合】

alt

在python内部某种情况下触发,回去扫描,可能存在循环引用的链表的每个元素,也会扫描每个元素的子元素,检查是否有循环引用,如果有则让双方的引用计数器-1,如果为0时则进行垃圾回收。

但是标记清除也会存在一定的问题:

  1. 什么时候扫描?

  2. 可能存在循环引用的链表扫描代价太大,每次扫描耗时太久。

为了解决上述问题python底层实现了分代回收机制。

3 分代回收

将可能存在循环应用的对象维护成3个链表,分成0代 、1代、2代。

alt

什么时候扫描?

  • 0代:0代中的对象达到了700个时扫描一次
  • 1代:0代扫描10次,1代扫描一次。
  • 2代:1代扫描10次,2代扫描一次。

最开始的对象都在0代中添加,直到对象个数到达700个,进行扫描一次,如果是循环引用,进行垃圾回收,如果打不是循环引用,则进行升级将0代中的对象放入到1代中,此时0代的数据为空。1代也会标记0代的数据扫描了一次。继续循环当0代的数据扫描了10次,1代才会扫描一次,同理,当1代扫描10次时才会进行2代扫描。

进行扫描时,0代的阈值是对象的个数,而1代与2代的阈值是扫描的次数。

进行分代回收,很好解决了什么进行扫描,扫描的代价太大的问题。

4. 垃圾回收机制小结

​ 在python中维护了一个refchain的双向链表,这个链表中存储着程序创建的所有对象,每种类型的对象都有一个ob_refcnt的引用计数器的值,当进行引用时计数器的个数+1,删除对象时引用计数器个数-1,最后大纲计数器的个数为0时进行垃圾回收【对象销毁(从内存中移除),双向循环链表中移除对象】。

但是,python中对于那些有对个元素的组成的对象可能会存在循环引用问题,为了解决这个问题python又引入了标记清除和分代回收,在其内部分为了4个链表。

  • rafchain

  • 2 代 1代扫描10次

  • 1代 0代扫描10次

  • 0代 ,对象个数达到700个

    在源码内部当达到各自的阈值时,就会触发扫描链表进行标记清除的动作(有循环引用引用计数器各自减1)。

    but,源码内部在上述的流程中提出了优化机制。

5 python缓存

5.1 池(int、字符串)

为了避免重复创建和销毁一些常见对象、维护池 。

  • int类型,不是基于free_list,而是维护一个small_ints链表保存常见数据(小数据池),小数据池范围:-5 <= value < 257。即:重复使用这个范围的整数时,不会重新开辟内存。
#启动解释器时,python内部会帮我们创建[-5,257]的所有对象,提前在内存块上开辟空间。
#我们创建这个对象时,内部不会再次开辟空间,而是直接去池中获取。
v1=7  #内存不会开辟空间,直接进入池中获取。
v2=9
v3=9
v1 = 38    # 去小数据池small_ints中获取38整数对象,将对象添加到refchain并让引用计数器+1。
print( id(v1))  #内存地址:4514343712
v2 = 38 # 去小数据池small_ints中获取38整数对象,将refchain中的对象的引用计数器+1。
print( id(v2) ) #内存地址:4514343712
  # 注意:在解释器启动时候-5~256就已经被加入到small_ints链表中且引用计数器初始化为1,代码中使用的值时直接去small_ints中拿来用并将引用计数器+1即可。另外,small_ints中的数据引用计数器永远不会为0(初始化时就设置为1了),所以也不会被销毁。

alt

  • str类型,维护unicode_latin1[256]字符池)链表,内部将所有的ascii字符缓存起来,以后使用时就不再反复创建。

      v1 = "A"
      print( id(v1) ) # 输出:4517720496
      del v1
      v2 = "A"
      print( id(v1) ) # 输出:4517720496
      # 除此之外,Python内部还对字符串做了驻留机制,针对那么只含有字母、数字、下划线的字符串(见源码Objects/codeobject.c),如果内存中已存在则不会重新在创建而是使用原来的地址里(不会像free_list那样一直在内存存活,只有内存中有才能被重复利用)。
      v1 = "wupeiqi"
      v2 = "wupeiqi"
      print(id(v1) == id(v2)) # 输出:True
    

5.2 free_list

​ 对象的引用计数器为0时,就会被销毁并释放内存。而实际上他不是这么的简单粗暴,因为反复的创建和销毁会使程序的执行效率变低。Python中引入了“缓存机制”机制。 例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list 的链表中,之后会再创建对象时不会在重新开辟内存,而是在free_list中将之前的对象来并重置内部的值来使用。

  • float类型,维护的free_list链表最多可缓存100个float对象。
v1 = 3.14    # 开辟内存来存储float对象,并将对象添加到refchain链表。
print( id(v1) ) # 内存地址:4436033488
del v1    # 引用计数器-1,如果为0则在rechain链表中移除,不销毁对象,而是将对象添加到float的free_list.
v2 = 9.999    # 优先去free_list中获取对象,并重置为9.999,如果free_list为空才重新开辟内存。
print( id(v2) ) # 内存地址:4436033488
  # 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。
  • list类型,维护的free_list数组最多可缓存80个list对象。
  v1 = [11,22,33]
  print( id(v1) ) # 输出:4517628816
  del v1
  v2 = ["武","沛齐"]
  print( id(v2) ) # 输出:4517628816
  • dict类型,维护的free_list数组最多可缓存80个dict对象。
  v1 = {"k1":123}
  print( id(v1) )  # 输出:4515998128
  del v1
  v2 = {"name":"武沛齐","age":18,"gender":"男"}
  print( id(v1) ) # 输出:4515998128
  • tuple类型,维护一个free_list数组且数组容量20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。
  v1 = (1,2)
  print( id(v1) )
  del v1  # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
  v2 = ("武沛齐","Alex")  # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
  print( id(v2) )

对于元祖类型,维护free_list,通过索引存放元祖,下标为0存储一个空元祖,下标为1存放含有一个元素的元祖,下标为2存放含有二个元素的元祖,依次类推....,索引[1,19]每个位置可以存放2000个元素。

alt

6 源码分析

详见:https://pythonav.com/wiki/detail/6/88/#2.%20C%E8%AF%AD%E8%A8%80%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

6.1 两个重要的结构体

#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;

这两个结构体PyObjectPyVarObject是基石,他们保存这其他数据类型公共部分,例如:每个类型的对象在创建时都有PyObject中的那4部分数据;list/set/tuple等由多个元素组成对象创建时都有PyVarObject中的那5部分数据。

6.2 常见类型结构体

  • float类型

     typedef struct {
          PyObject_HEAD
          double ob_fval;
      } PyFloatObject;
    
  • int 类型

     struct _longobject {
          PyObject_VAR_HEAD
          digit ob_digit[1];
      };
      /* Long (arbitrary precision) integer object interface */
      typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
    
  • str 类型

    typedef struct {
          PyObject_HEAD
          Py_ssize_t length;          /* Number of code points in the string */
          Py_hash_t hash;             /* Hash value; -1 if not set */
          struct {
              unsigned int interned:2;
              /* Character size:
             - PyUnicode_WCHAR_KIND (0):
               * character type = wchar_t (16 or 32 bits, depending on the
                 platform)
             - PyUnicode_1BYTE_KIND (1):
               * character type = Py_UCS1 (8 bits, unsigned)
               * all characters are in the range U+0000-U+00FF (latin1)
               * if ascii is set, all characters are in the range U+0000-U+007F
                 (ASCII), otherwise at least one character is in the range
                 U+0080-U+00FF
             - PyUnicode_2BYTE_KIND (2):
               * character type = Py_UCS2 (16 bits, unsigned)
               * all characters are in the range U+0000-U+FFFF (BMP)
               * at least one character is in the range U+0100-U+FFFF
             - PyUnicode_4BYTE_KIND (4):
               * character type = Py_UCS4 (32 bits, unsigned)
               * all characters are in the range U+0000-U+10FFFF
               * at least one character is in the range U+10000-U+10FFFF
             */
              unsigned int kind:3;
              unsigned int compact:1;
              unsigned int ascii:1;
              unsigned int ready:1;
              unsigned int :24;
          } state;
          wchar_t *wstr;              /* wchar_t representation (null-terminated) */
      } PyASCIIObject;
      typedef struct {
          PyASCIIObject _base;
          Py_ssize_t utf8_length;     /* Number of bytes in utf8, excluding the
                                       * terminating \0. */
          char *utf8;                 /* UTF-8 representation (null-terminated) */
          Py_ssize_t wstr_length;     /* Number of code points in wstr, possible
                                       * surrogates count as two code points. */
      } PyCompactUnicodeObject;
      typedef struct {
          PyCompactUnicodeObject _base;
          union {
              void *any;
              Py_UCS1 *latin1;c
              Py_UCS2 *ucs2;
              Py_UCS4 *ucs4;
          } data;                     /* Canonical, smallest-form Unicode buffer */
      } PyUnicodeObject;
    
  • list类型

     typedef struct {
          PyObject_VAR_HEAD
          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_values;
      } PyDictObject;
    

    过常见结构体可以基本了解到本质上每个对象内部会存储的数据。

    扩展:在结构体部分你应该发现了str类型比较繁琐,那是因为python字符串在处理时需要考虑到编码的问题,在内部规定(见源码结构体):

    • 字符串只包含ascii,则每个字符用1个字节表示,即:latin1

    • 字符串包含中文等,则每个字符用2个字节表示,即:ucs2

    • 字符串包含emoji等,则每个字符用4个字节表示,即:ucs4