Python的內(nèi)存管理及垃圾回收機(jī)制

首先要記住一句話:

“Python采用的是引用計(jì)數(shù)機(jī)制為主,標(biāo)記清除和分代收集兩種機(jī)制為輔的策略疼阔。”

引用計(jì)數(shù)器

首先來(lái)了解一下引用計(jì)數(shù)器半夷,因?yàn)槭?strong>以引用計(jì)數(shù)器為主的機(jī)制婆廊。

1.環(huán)狀雙向鏈表

在Python中,有一個(gè)環(huán)狀的雙向鏈表叫refchain巫橄,任何對(duì)象都會(huì)被放在這個(gè)環(huán)狀雙向鏈表中淘邻。

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

上面的代碼一執(zhí)行,會(huì)創(chuàng)建三個(gè)對(duì)象湘换,一個(gè)字符串對(duì)象宾舅,一個(gè)整型對(duì)象,一個(gè)列表對(duì)象彩倚,這三個(gè)對(duì)象都會(huì)被放到這個(gè)鏈表中筹我。

當(dāng)python代碼遇到name = "cat",內(nèi)部會(huì)創(chuàng)建一些數(shù)據(jù)(C語(yǔ)言源碼是創(chuàng)建了一個(gè)結(jié)構(gòu)體):上一個(gè)對(duì)象帆离,下一個(gè)對(duì)象蔬蕊,類(lèi)型,引用的個(gè)數(shù)等等哥谷,當(dāng)前對(duì)象的類(lèi)型是字符串岸夯,引用的個(gè)數(shù)是1,因?yàn)閚ame這個(gè)變量名引用了當(dāng)前這個(gè)對(duì)象们妥,如果new = name猜扮,那么這個(gè)引用計(jì)數(shù)會(huì)加一。

如果這個(gè)對(duì)象是由多個(gè)元素組成的监婶,還會(huì)有一個(gè)值記錄它的元素個(gè)數(shù)旅赢。

C的源碼:

#define PyObject_HEAD       PyObject ob_base;
#define PyObject_VAR_HEAD      PyVarObject ob_base;
// 宏定義,包含 上一個(gè)压储、下一個(gè)鲜漩,用于構(gòu)造雙向鏈表用源譬。(放到refchain鏈表中時(shí)集惋,要用到)
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;
typedef struct _object {
    _PyObject_HEAD_EXTRA // 用于構(gòu)造雙向鏈表
    Py_ssize_t ob_refcnt;  // 引用計(jì)數(shù)器
    struct _typeobject *ob_type;    // 數(shù)據(jù)類(lèi)型
} PyObject;
typedef struct {
    PyObject ob_base;   // PyObject對(duì)象
    Py_ssize_t ob_size; /* Number of items in variable part,即:元素個(gè)數(shù) */
} PyVarObject;

如果是不同類(lèi)型的數(shù)據(jù)踩娘,內(nèi)部會(huì)創(chuàng)建以下的內(nèi)容:

  • float類(lèi)型
  typedef struct {
      PyObject_HEAD
      double ob_fval;
  } PyFloatObject;
  • int類(lèi)型
  struct _longobject {
      PyObject_VAR_HEAD
      digit ob_digit[1];
  };
  /* Long (arbitrary precision) integer object interface */
  typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
  • str類(lèi)型
  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類(lèi)型
  typedef struct {
      PyObject_VAR_HEAD
      PyObject **ob_item;
      Py_ssize_t allocated;
  } PyListObject;
  • tuple類(lèi)型
  typedef struct {
      PyObject_VAR_HEAD
      PyObject *ob_item[1];
  } PyTupleObject;
  • dict類(lèi)型
  typedef struct {
      PyObject_HEAD
      Py_ssize_t ma_used;
      PyDictKeysObject *ma_keys;
      PyObject **ma_values;
  } PyDictObject;

其中:

  • _ob_next : refchian中的上一個(gè)對(duì)象
  • _ob_prev:refchain中的下一個(gè)對(duì)象
  • ob_refnt:引用計(jì)數(shù)器
  • ob_type:是當(dāng)前對(duì)象的類(lèi)型
  • ob_fval:是這個(gè)對(duì)象的值

當(dāng)python運(yùn)行程序時(shí)刮刑,會(huì)根據(jù)數(shù)據(jù)類(lèi)型的不同找到它對(duì)應(yīng)的結(jié)構(gòu)體喉祭,根據(jù)結(jié)構(gòu)體中的字段來(lái)進(jìn)行創(chuàng)建相關(guān)數(shù)據(jù),然后將這個(gè)對(duì)象添加到refchain中雷绢。

大體機(jī)制:
每個(gè)對(duì)象中有ob_refcnt就是引用計(jì)數(shù)器泛烙,默認(rèn)值為1,當(dāng)有其他變量引用對(duì)象時(shí)翘紊,引用計(jì)數(shù)器的值會(huì)增加蔽氨,如果引用這個(gè)對(duì)象的變量被刪除或者引用別的對(duì)象了,那么這個(gè)引用計(jì)數(shù)器的值會(huì)減小帆疟,當(dāng)引用計(jì)數(shù)器的值變?yōu)?時(shí)鹉究,意味著沒(méi)有變量在使用這個(gè)對(duì)象了,那么這個(gè)對(duì)象就變成了需要被刪除的垃圾踪宠,系統(tǒng)就會(huì)將這個(gè)對(duì)象從refchian里面移除自赔,將對(duì)象銷(xiāo)毀,把這塊內(nèi)存還給系統(tǒng)柳琢。

2.單純使用引用計(jì)數(shù)器進(jìn)行垃圾回收的問(wèn)題:循環(huán)引用

如果是這樣的一個(gè)代碼:

# refchian中創(chuàng)建一個(gè)列表對(duì)象绍妨,因?yàn)関1=對(duì)象,所以對(duì)象的引用計(jì)數(shù)器為1
v1 = [11,22,33]
# 同理為1
v2 = [44,55,66]
# 把v2追加到v1中柬脸,那么[11,22,33]的引用計(jì)數(shù)器就會(huì)加一他去,變?yōu)?
v1.append(v2)
# 同理為2
v2.append(v1)

這時(shí)候,如果刪除了變量v1和變量v2:

# [11,22,33] 的引用計(jì)數(shù)器減一倒堕,變?yōu)?
del v1
# [44,55,66]同理為1
del v2

這時(shí)候就會(huì)有一個(gè)問(wèn)題孤页,就是已經(jīng)沒(méi)有變量引用對(duì)象[11,22,33]和[44,55,66]了,但是因?yàn)樗麄兊囊糜?jì)數(shù)器不為0涩馆,這兩個(gè)對(duì)象的內(nèi)存空間沒(méi)有被回收行施,所以他們會(huì)永遠(yuǎn)在內(nèi)存中,而又永遠(yuǎn)不會(huì)被使用到魂那。

所以為了解決這個(gè)問(wèn)題蛾号,就有了標(biāo)記清除。

標(biāo)記清除

標(biāo)記清除就是為了解決引用計(jì)數(shù)器循環(huán)引用的問(wèn)題涯雅。

實(shí)習(xí)方法就是在Python的底層再去維護(hù)一個(gè)環(huán)形雙向鏈表鲜结,這個(gè)鏈表用來(lái)存放可能存在循環(huán)引用問(wèn)題的對(duì)象。(只有對(duì)象可以再放其他元素的對(duì)象才會(huì)出現(xiàn)循環(huán)引用問(wèn)題活逆,列表精刷,字典,元組和集合)

在Python內(nèi)部蔗候,在某種情況下怒允,回去掃描這個(gè)存放可能存在循環(huán)引用問(wèn)題的對(duì)象的鏈表,如果發(fā)現(xiàn)有循環(huán)引用锈遥,就把雙方的引用計(jì)數(shù)器都減一纫事,如果引用計(jì)數(shù)器減為0勘畔,回收內(nèi)存。

但是標(biāo)記清除也存在問(wèn)題:

  • 什么時(shí)候會(huì)掃描一次
  • 掃描一次存在耗時(shí)久的問(wèn)題

所以又引入了分代回收丽惶。

分代回收

實(shí)現(xiàn)方式就是把標(biāo)記清除的那個(gè)鏈表分成了三個(gè)鏈表炫七,這三個(gè)鏈表分別是:0代,1代钾唬,2代万哪。

  • 當(dāng)0代中的對(duì)象個(gè)數(shù)超過(guò)700個(gè),掃描一次0代抡秆。
  • 0代如果掃描10次壤圃,則1代掃描一次。
  • 1代掃描10次琅轧,2代掃描一次伍绳。

三個(gè)鏈表的閾值是不同的,0代是對(duì)象個(gè)數(shù)乍桂,1代和2代都是前一代的掃描次數(shù)冲杀。

總結(jié)

在Python中維護(hù)了一個(gè)叫refchain的雙向環(huán)狀鏈表,這個(gè)鏈表用來(lái)存儲(chǔ)程序創(chuàng)建的所有對(duì)象睹酌,每種類(lèi)型的對(duì)象都有一個(gè)ob_refcnt引用計(jì)數(shù)器的值权谁,當(dāng)有變量引用對(duì)象時(shí),引用計(jì)數(shù)器的值就會(huì)加一憋沿,當(dāng)引用對(duì)象的變量減少了的時(shí)候旺芽,引用計(jì)數(shù)器的值就會(huì)減一,當(dāng)引用計(jì)數(shù)器變?yōu)?時(shí)辐啄,就會(huì)進(jìn)行垃圾回收采章。

但是在Python中,對(duì)于有多個(gè)元素組成的對(duì)象壶辜,可能還會(huì)有循環(huán)引用的問(wèn)題悯舟,為了解決這個(gè)問(wèn)題,Python又引用了標(biāo)記清除和分代回收砸民,所以在Python內(nèi)部實(shí)際上要維護(hù)四個(gè)雙向環(huán)狀鏈表抵怎,分別是:

  • refchian
  • 0代 700個(gè)
  • 1代 0代掃描10次
  • 2代 1代掃描10次
    在源碼內(nèi)部,當(dāng)達(dá)到各自的閾值時(shí)岭参,就會(huì)掃描鏈表反惕,發(fā)現(xiàn)循環(huán)引用就會(huì)想相關(guān)對(duì)象的引用計(jì)數(shù)器減一,如果引用計(jì)數(shù)器的值被減為0演侯,那么這個(gè)對(duì)象就會(huì)被銷(xiāo)毀姿染,這個(gè)對(duì)象占用的內(nèi)存空間就會(huì)被回收。

優(yōu)化:緩存

在Python內(nèi)部蚌本,源碼對(duì)上述過(guò)程進(jìn)行了優(yōu)化盔粹,這個(gè)優(yōu)化就是緩存。

1.內(nèi)存池

為了避免重復(fù)創(chuàng)建和銷(xiāo)毀一些常見(jiàn)對(duì)象程癌,就會(huì)維護(hù)一個(gè)內(nèi)存池舷嗡。

在啟動(dòng)Python解釋器時(shí),內(nèi)部會(huì)自動(dòng)為我們創(chuàng)建-5~256這些數(shù)字放到內(nèi)存池中嵌莉,如果有變量需要指向這些值进萄,內(nèi)存不會(huì)開(kāi)辟新的內(nèi)存,直接從內(nèi)存池中取锐峭。

所以如果:

v1 = 8
v2 = 8

打印一下v1和v2的id中鼠,會(huì)發(fā)現(xiàn)他們的id是相同的。

而且內(nèi)存池中的對(duì)象沿癞,他們的引用計(jì)數(shù)器不會(huì)變?yōu)?援雇,因?yàn)槌跏蓟臅r(shí)候,他們的引用計(jì)數(shù)器就為1椎扬,這時(shí)候沒(méi)有變量引用他們惫搏,所以當(dāng)引用他們的變量引用后又不再引用他們的時(shí)候,他們的引用計(jì)數(shù)器也不會(huì)變?yōu)?蚕涤。

2.free_list

當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)器變?yōu)?時(shí)筐赔,先不回收,而是把這個(gè)對(duì)象放到free_list中揖铜,當(dāng)作緩存茴丰,這樣再創(chuàng)建對(duì)象時(shí),就不開(kāi)辟新的內(nèi)存天吓,直接使用free_list中的對(duì)象的內(nèi)存贿肩,把這塊內(nèi)存初始化,再把這個(gè)對(duì)象放到refchain中龄寞。

當(dāng)然這個(gè)緩沖池也是有大小限制的尸曼,如果一個(gè)對(duì)象的引用計(jì)數(shù)器變?yōu)?,而此時(shí)緩沖池也已經(jīng)滿了萄焦,那么這個(gè)對(duì)象還是會(huì)被直接銷(xiāo)毀的控轿。

float,list拂封,tuple茬射,dict采用這種機(jī)制。

  • float類(lèi)型冒签,維護(hù)的free_list鏈表最多可緩存100個(gè)float對(duì)象在抛。
  • list類(lèi)型,維護(hù)的free_list鏈表最多可緩存80個(gè)list對(duì)象萧恕。
  • dict類(lèi)型刚梭,維護(hù)的free_list鏈表最多可緩存80個(gè)dict對(duì)象肠阱。

tuple類(lèi)型的比較特殊,可以理解為如果是tuple類(lèi)型的朴读,free_list的容量為20屹徘,這時(shí)候的free_list有20個(gè)index,index從0到19衅金,每個(gè)索引位置都存放了一個(gè)鏈表噪伊,index為0的位置,存放的是空元組氮唯,index為1的位置存放的是元組長(zhǎng)度為1的元組鉴吹,這樣以此類(lèi)推。每一個(gè)鏈表都可以存放2000個(gè)元組惩琉。

字符串類(lèi)型的有兩種優(yōu)化方式:
1.字符池
2.字符串駐留機(jī)制

  • 字符池就是和int的內(nèi)存池類(lèi)似豆励,python內(nèi)部會(huì)將ASCII的所有字符存在一個(gè)叫unicode_latin[256]的鏈表中。
  • 字符串駐留機(jī)制:python會(huì)將只有字母瞒渠、數(shù)字肆糕、下劃線并且長(zhǎng)度不大于20的字符串進(jìn)行駐留,放到內(nèi)存在孝,如果下次再創(chuàng)建一個(gè)一模一樣的值诚啃,就不再開(kāi)辟新的內(nèi)存空間。

詳見(jiàn):https://pythonav.com/wiki/detail/6/88/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末私沮,一起剝皮案震驚了整個(gè)濱河市始赎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仔燕,老刑警劉巖造垛,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異晰搀,居然都是意外死亡五辽,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)外恕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杆逗,“玉大人,你說(shuō)我怎么就攤上這事鳞疲∽锝迹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵尚洽,是天一觀的道長(zhǎng)悔橄。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么癣疟? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任挣柬,我火速辦了婚禮,結(jié)果婚禮上睛挚,老公的妹妹穿的比我還像新娘邪蛔。我一直安慰自己,他們只是感情好竞川,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布店溢。 她就那樣靜靜地躺著叁熔,像睡著了一般委乌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荣回,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天遭贸,我揣著相機(jī)與錄音,去河邊找鬼心软。 笑死壕吹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的删铃。 我是一名探鬼主播耳贬,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼猎唁!你這毒婦竟也來(lái)了咒劲?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤诫隅,失蹤者是張志新(化名)和其女友劉穎腐魂,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體逐纬,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛔屹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了豁生。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兔毒。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甸箱,靈堂內(nèi)的尸體忽然破棺而出眼刃,到底是詐尸還是另有隱情,我是刑警寧澤摇肌,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布擂红,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏昵骤。R本人自食惡果不足惜树碱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望变秦。 院中可真熱鬧成榜,春花似錦、人聲如沸蹦玫。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)樱溉。三九已至挣输,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間福贞,已是汗流浹背撩嚼。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挖帘,地道東北人完丽。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拇舀,于是被迫代替她去往敵國(guó)和親逻族。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354