大話Python垃圾回收機制

在面試當中常常被問Python的垃圾回收是怎麼作的,但也只是一句話:
引用計數器爲主、分代回收和標記清除爲輔python

垃圾回收

1.1 refchain

在Python的C源碼中有一個refchain環狀雙向鏈表,Python程序當中一旦建立對象都會把這個對象添加到refchain這個鏈表當中,保存着全部的對象。git

name = "皮卡丘"
width = 5

雙向環形鏈表

1.2引用計數器

refchain中全部對象內部都有一個ob_refcnt用來保存當前對象的引用計數器面試

name = "皮卡丘"
width = 5
nickname = name

上述代碼表示內存中有5和」皮卡丘「兩個值,他們的引用計數器分別爲一、2
TIM截圖20200511093718.png數組

當值被屢次引用時候,不會再內存中重複建立數據,而是引用計數器+1。當對象被銷燬時候同時會讓引用計數器-1,若是引用計數器爲0,則將對象從refchain鏈表中刪除,同時在內存中進行銷燬(暫時不考慮緩存等特殊狀況)。緩存

name = "皮卡丘"
nickname = name # 對象」皮卡丘「的引用計數器+1
del name  對象"皮卡丘"的引用計數器-1

def run(arg):
    print(arg)

run(nickname) # 剛開始執行函數時,對象」皮卡丘「引用計數器+1,當函數執行完畢以後,對象引用計數器-1

name_list = ["張三","法外狂徒",name] # 對象」皮卡丘「的引用計數器+1

可是這樣仍是存在一個BUG,當出現循環引用的時候,就會沒法正常的回收一些數據,例如app

v1 = [11,22,33]        # refchain中建立一個列表對象,因爲v1=對象,因此列表引對象用計數器爲1.
v2 = [44,55,66]        # refchain中再建立一個列表對象,因v2=對象,因此列表對象引用計數器爲1.
v1.append(v2)        # 把v2追加到v1中,則v2對應的[44,55,66]對象的引用計數器加1,最終爲2.
v2.append(v1)        # 把v1追加到v1中,則v1對應的[11,22,33]對象的引用計數器加1,最終爲2.
del v1    # 引用計數器-1
del v2    # 引用計數器-1

對於上面的代碼,執行del操做以後,沒有變量再會去使用那兩個列表對象,但因爲循環引用的問題,他們的引用計數器不爲0,因此他們的狀態:永遠不會被使用、也不會被銷燬。項目中若是這種代碼太多,就會致使內存一直被消耗,直到內存被耗盡,程序崩潰。函數

1.3標記清除&分代回收

標記清除:建立特殊鏈表專門用於保存 列表、元組、字典、集合、自定義類等對象,以後再去檢查這個鏈表中的對象是否存在循環引用,若是存在則讓雙方的引用計數器均 - 1 。
分代回收:對標記清楚中的鏈表進行優化,將那些可能存在循環引用的對象拆分到3個鏈表,鏈表成爲0/1/2三代,每代均可以存儲對象和閾值,當達到閾值時,就會對相應的鏈表中的每一個對象作一次掃描,除循環引用各自減1而且銷燬引用計數器爲0的對象。`源碼分析

// 分代的C源碼
#define NUM_GENERATIONS 3
struct gc_generation generations[NUM_GENERATIONS] = {
    /* PyGC_Head,                                    threshold,    count */
    {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)},   700,        0}, // 0代
    {{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)},   10,         0}, // 1代
    {{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)},   10,         0}, // 2代
};
  • 0代,count表示0代鏈表中對象的數量,threshold表示0代鏈表對象個數閾值,超過則執行一次0代掃描檢查。
  • 1代,count表示0代鏈表掃描的次數,threshold表示0代鏈表掃描的次數閾值,超過則執行一次1代掃描檢查。
  • 2代,count表示1代鏈表掃描的次數,threshold表示1代鏈表掃描的次數閾值,超過則執行一2代掃描檢查。

1.4緩存機制

實際上他不是這麼的簡單粗暴,由於反覆的建立和銷燬會使程序的執行效率變低。Python中引入了「緩存機制」機制。
例如:引用計數器爲0時,不會真正銷燬對象,而是將他放到一個名爲free_list的鏈表中,以後會再建立對象時不會在從新開闢內存,而是在free_list中將以前的對象來並重置內部的值來使用。優化

  • float類型,維護的free_list鏈表最多可緩存100個float對象。
*   1.   `v1 =  3.14  # 開闢內存來存儲float對象,並將對象添加到refchain鏈表。`
    2.   `print( id(v1)  )  # 內存地址:4436033488`
    3.   `del v1 # 引用計數器-1,若是爲0則在rechain鏈表中移除,不銷燬對象,而是將對象添加到float的free_list.`
    4.   `v2 =  9.999  # 優先去free_list中獲取對象,並重置爲9.999,若是free_list爲空才從新開闢內存。`
    5.   `print( id(v2)  )  # 內存地址:4436033488`
    
    7.   `# 注意:引用計數器爲0時,會先判斷free_list中緩存個數是否滿了,未滿則將對象緩存,已滿則直接將對象銷燬。`
  • int類型,不是基於free_list,而是維護一個small_ints鏈表保存常見數據(小數據池),小數據池範圍:-5 <= value < 257。即:重複使用這個範圍的整數時,不會從新開闢內存。
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了),因此也不會被銷燬。
  • 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
  • list類型,維護的free_list數組最多可緩存80個list對象。
v1 = [11,22,33]
  print( id(v1) ) # 輸出:4517628816
  del v1
  v2 = ["小豬","佩奇"]
  print( id(v2) ) # 輸出:4517628816
  • tuple類型,維護一個free_list數組且數組容量20,數組中元素能夠是鏈表且每一個鏈表最多能夠容納2000個元組對象。元組的free_list數組在存儲數據時,是按照元組能夠容納的個數爲索引找到free_list數組中對應的鏈表,並添加到鏈表中。
v1 = (1,2)
  print( id(v1) )
  del v1  # 因元組的數量爲2,因此會把這個對象緩存到free_list[2]的鏈表中。
  v2 = ("小豬","佩奇")  # 不會從新開闢內存,而是去free_list[2]對應的鏈表中拿到一個對象來使用。
  print( id(v2) )
  • dict類型,維護的free_list數組最多可緩存80個dict對象。
v1 = {"k1":123}
  print( id(v1) )  # 輸出:4515998128
  del v1
  v2 = {"name":"武沛齊","age":18,"gender":"男"}
  print( id(v1) ) # 輸出:4515998128

2 C語言源碼分析

2.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部分數據。ui

2.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;
          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

寫在最後

問渠哪得清如許 
惟有源頭活水來
相關文章
相關標籤/搜索