Python的内存管理及垃圾回收机制

首先要记住一句话:

“Python采用的是引用计数机制为主,标记清除和分代收集两种机制为辅的策略。”

引用计数器

首先来了解一下引用计数器,因为是以引用计数器为主的机制。

1.环状双向链表

在Python中,有一个环状的双向链表叫refchain,任何对象都会被放在这个环状双向链表中。

name = "cat"
age = 10
hobby = ["eat fish", "sleep"]

上面的代码一执行,会创建三个对象,一个字符串对象,一个整型对象,一个列表对象,这三个对象都会被放到这个链表中。

当python代码遇到name = "cat",内部会创建一些数据(C语言源码是创建了一个结构体):上一个对象,下一个对象,类型,引用的个数等等,当前对象的类型是字符串,引用的个数是1,因为name这个变量名引用了当前这个对象,如果new = name,那么这个引用计数会加一。

如果这个对象是由多个元素组成的,还会有一个值记录它的元素个数。

C的源码:

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

如果是不同类型的数据,内部会创建以下的内容:

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

其中:

  • _ob_next : refchian中的上一个对象
  • _ob_prev:refchain中的下一个对象
  • ob_refnt:引用计数器
  • ob_type:是当前对象的类型
  • ob_fval:是这个对象的值

当python运行程序时,会根据数据类型的不同找到它对应的结构体,根据结构体中的字段来进行创建相关数据,然后将这个对象添加到refchain中。

大体机制:
每个对象中有ob_refcnt就是引用计数器,默认值为1,当有其他变量引用对象时,引用计数器的值会增加,如果引用这个对象的变量被删除或者引用别的对象了,那么这个引用计数器的值会减小,当引用计数器的值变为0时,意味着没有变量在使用这个对象了,那么这个对象就变成了需要被删除的垃圾,系统就会将这个对象从refchian里面移除,将对象销毁,把这块内存还给系统。

2.单纯使用引用计数器进行垃圾回收的问题:循环引用

如果是这样的一个代码:

# refchian中创建一个列表对象,因为v1=对象,所以对象的引用计数器为1
v1 = [11,22,33]
# 同理为1
v2 = [44,55,66]
# 把v2追加到v1中,那么[11,22,33]的引用计数器就会加一,变为2
v1.append(v2)
# 同理为2
v2.append(v1)

这时候,如果删除了变量v1和变量v2:

# [11,22,33] 的引用计数器减一,变为1
del v1
# [44,55,66]同理为1
del v2

这时候就会有一个问题,就是已经没有变量引用对象[11,22,33]和[44,55,66]了,但是因为他们的引用计数器不为0,这两个对象的内存空间没有被回收,所以他们会永远在内存中,而又永远不会被使用到。

所以为了解决这个问题,就有了标记清除。

标记清除

标记清除就是为了解决引用计数器循环引用的问题。

实习方法就是在Python的底层再去维护一个环形双向链表,这个链表用来存放可能存在循环引用问题的对象。(只有对象可以再放其他元素的对象才会出现循环引用问题,列表,字典,元组和集合)

在Python内部,在某种情况下,回去扫描这个存放可能存在循环引用问题的对象的链表,如果发现有循环引用,就把双方的引用计数器都减一,如果引用计数器减为0,回收内存。

但是标记清除也存在问题:

  • 什么时候会扫描一次
  • 扫描一次存在耗时久的问题

所以又引入了分代回收。

分代回收

实现方式就是把标记清除的那个链表分成了三个链表,这三个链表分别是:0代,1代,2代。

  • 当0代中的对象个数超过700个,扫描一次0代。
  • 0代如果扫描10次,则1代扫描一次。
  • 1代扫描10次,2代扫描一次。

三个链表的阈值是不同的,0代是对象个数,1代和2代都是前一代的扫描次数。

总结

在Python中维护了一个叫refchain的双向环状链表,这个链表用来存储程序创建的所有对象,每种类型的对象都有一个ob_refcnt引用计数器的值,当有变量引用对象时,引用计数器的值就会加一,当引用对象的变量减少了的时候,引用计数器的值就会减一,当引用计数器变为0时,就会进行垃圾回收。

但是在Python中,对于有多个元素组成的对象,可能还会有循环引用的问题,为了解决这个问题,Python又引用了标记清除和分代回收,所以在Python内部实际上要维护四个双向环状链表,分别是:

  • refchian
  • 0代 700个
  • 1代 0代扫描10次
  • 2代 1代扫描10次
    在源码内部,当达到各自的阈值时,就会扫描链表,发现循环引用就会想相关对象的引用计数器减一,如果引用计数器的值被减为0,那么这个对象就会被销毁,这个对象占用的内存空间就会被回收。

优化:缓存

在Python内部,源码对上述过程进行了优化,这个优化就是缓存。

1.内存池

为了避免重复创建和销毁一些常见对象,就会维护一个内存池。

在启动Python解释器时,内部会自动为我们创建-5~256这些数字放到内存池中,如果有变量需要指向这些值,内存不会开辟新的内存,直接从内存池中取。

所以如果:

v1 = 8
v2 = 8

打印一下v1和v2的id,会发现他们的id是相同的。

而且内存池中的对象,他们的引用计数器不会变为0,因为初始化的时候,他们的引用计数器就为1,这时候没有变量引用他们,所以当引用他们的变量引用后又不再引用他们的时候,他们的引用计数器也不会变为0。

2.free_list

当一个对象的引用计数器变为0时,先不回收,而是把这个对象放到free_list中,当作缓存,这样再创建对象时,就不开辟新的内存,直接使用free_list中的对象的内存,把这块内存初始化,再把这个对象放到refchain中。

当然这个缓冲池也是有大小限制的,如果一个对象的引用计数器变为0,而此时缓冲池也已经满了,那么这个对象还是会被直接销毁的。

float,list,tuple,dict采用这种机制。

  • float类型,维护的free_list链表最多可缓存100个float对象。
  • list类型,维护的free_list链表最多可缓存80个list对象。
  • dict类型,维护的free_list链表最多可缓存80个dict对象。

tuple类型的比较特殊,可以理解为如果是tuple类型的,free_list的容量为20,这时候的free_list有20个index,index从0到19,每个索引位置都存放了一个链表,index为0的位置,存放的是空元组,index为1的位置存放的是元组长度为1的元组,这样以此类推。每一个链表都可以存放2000个元组。

字符串类型的有两种优化方式:
1.字符池
2.字符串驻留机制

  • 字符池就是和int的内存池类似,python内部会将ASCII的所有字符存在一个叫unicode_latin[256]的链表中。
  • 字符串驻留机制:python会将只有字母、数字、下划线并且长度不大于20的字符串进行驻留,放到内存,如果下次再创建一个一模一样的值,就不再开辟新的内存空间。

详见:https://pythonav.com/wiki/detail/6/88/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335