Python垃圾回收機制

ckxllf發表於2020-05-21

  引用計數器為主

  標記清除和分代回收為輔

  + 快取機制

  1. 引用計數器

  1.1 環狀雙向連結串列 refchain

  在python程式中建立的任何物件都會放在refchain連結串列中。

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

  name = "阿瑋"

  age = 18

  hobby = ["健身", "美女"]

  內部會建立一些資料 [ 上一個物件、下一個物件、型別、引用個數 ]

  name = "阿瑋"

  new = name # 引用個數變成2

  內部會建立一些資料 [ 上一個物件、下一個物件、型別、引用個數、val=18 ]

  age = 18

  內部會建立一些資料 [ 上一個物件、下一個物件、型別、引用個數、items=元素、元素個數 ]

  hobby = ["健身", "美女"]

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

  在C原始碼中如何體現每個物件中都有的相同的值:PyObject結構體(4個值)。

  有多個元素組成的物件:PyObject結構體(4個值)+ ob_size = PyVarObject。

  1.2 型別封裝結構體

  float型別

  typedef struct {

  PyObject_HEAD;

  double ob_fval;

  };

  data = 3.14;

  內部會建立:

  _ob_next = refchain中的下一個物件

  _ob_prev = refchain中的上一個物件

  ob_refcnt = 1

  ob_type = float

  ob_fval = 3.14

  int型別

  struct _longobect {

  PyObject_VAR_HEAD;

  digit ob_dit[1];

  };

  /* Long (arbitrary precision) integer object interface */

  typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

  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;

  PyDictKeyObject *ma_keys;

  PyObject **ma_values;

  } PyDictObject;

  1.3 引用計數器

  v1 = 3.14

  v2 = 999

  v3 = (1,2,3)

  當python程式執行時,會根據資料型別的不同找到其結構體,根據結構體中的欄位來進行建立相關的資料,然後將物件新增到refchain雙向連結串列中。

  在C原始碼中有兩個關鍵的結構體:PyObject、PyVarObject。

  每個物件中有 ob_refcnt 就是引用計數器,值預設為1,當有其他變數引用這個物件時,引用計數器就會發生變化。

  引用

  a = 99999

  b = a

  # 此時 99999 這個物件引用計數器的值為2

  '''

  下面情況會導致引用計數器+1:

  1.物件被建立,如 a = 2

  2.物件被引用,如 b = a

  3.物件被作為引數,傳入到一個函式中

  4.物件作為一個元素,儲存在容器中

  可以透過sys包中的getrefcount()來獲取一個名稱所引用的物件當前的引用計數器的值(注意這裡getrefcount()本身會使得引用計數器+1)

  '''

  刪除引用

  a = 99999

  b = a

  # b變數刪除,b對應物件的引用計數器-1

  def b

  # a變數刪除,a對應物件的引用計數器-1

  '''

  下面情況會導致引用計數器-1:

  1.變數被顯示銷燬 del

  2.變數被賦予新的物件

  3.一個物件離開它的作用域

  4.物件所在的容器被銷燬或從容器中刪除物件

  '''

  # 當一個物件的引用計數器為0時,意味著沒有人再使用這個物件了,這個物件就是垃圾,垃圾回收。

  # 回收:1.物件從rechain連結串列移出。2.將物件銷燬,記憶體歸還。

  1.4 迴圈引用問題

  由於 v1 指向的物件引用了 v2,v2 指向的物件也引用了 v1,當將 v1、v2 兩個變數刪除時,雖然引用計數器會減1,但是兩個物件間還存在迴圈引用,而此時已經沒有變數能去指向它們,這兩個物件就會在記憶體中常駐無法處理。

  2. 標記清除

  目的:為了解決引用計數器迴圈引用的問題。

  實現:在python的底層再維護一個連結串列,連結串列中專門放哪些可能存在迴圈應用的物件(容器類物件:list、tuple、dict、set)。

  在Python內部某種情況下觸發,會去掃描可能存在迴圈引用的連結串列中的每個元素,檢查是否有迴圈引用,如果有則讓雙方的引用計數器-1;如果是0則垃圾回收。

  2.1 標記階段

  遍歷所有物件,如果是可達的(reachable),也就是還有物件引用它,那麼就將該物件標記為可達

  該階段從某個物件開始掃描(而不是從變數),如果變數A引用了變數B,則將變數B的引用計數器-1(指的是gc_ref),然後掃描變數B 鄭州人流手術醫院

  如圖所示,link1、link2、link3形成了一個引用環,link4自引用。從link1開始掃描,link1引用了link2,則link2的gc_ref-1,接著掃描link2…

  像這也將連結串列中所有物件考察一遍後,兩個連結串列中的物件ref_count和gc_ref圖如所示,這一步操作就相當於解除了迴圈引用對引用計數器的影響

  如果gc_ref為0,則將物件標記為 GC_TENTATIVELY_UNREACHABLE,並且被移至”Unreachable“連結串列中,如下圖link3、link4(我覺得link2應該也是)

  如果gc_ref不為0,那麼這個物件會被標記為可達的GC_REACHABLE,同時當gc發現有一個節點是可達的,那麼它會遞迴式的從該節點觸發將所有可達的節點標記為GC_REACHABLE,這樣把link2、link3救回來

  2.2 清除階段

  將被標記成 GC_UNREACHABLE 的物件銷燬,記憶體歸還(也就是Unreachable連結串列中的物件)

  2.3 標記清除的問題

  在標記清除演算法開始後,會暫停整個應用程式,等待標記清除結束後才會恢復應用的執行,且對迴圈引用的掃描代價大,每次掃描耗時可能很久

  3. 分代回收

  將可能存在迴圈引用的物件維護成3個連結串列:

  0代:0代中物件個數達到700個掃描一次

  1代:0代掃描10次,則1代掃描一次

  2代:1代掃描10次,則2代掃描一次

  4. 小結

  在python中維護了一個refchain的雙向環狀連結串列,這個連結串列中儲存程式建立的所有物件,每種型別的物件都有一個ob_refcnt引用計數器的值,當引用計數器變為0時會進行垃圾回收(物件銷燬、refchain中移出)。

  但是,在python中對於那些可以有多個元素組成的物件可能會存在迴圈引用的問題,為了解決這個問題,python又引入了標記清除和分代回收,在其內部維護了4個連結串列,分別為:

  refchain

  2代

  1代

  0代

  在原始碼內部,當達到各自的閾值時,就會觸發掃描連結串列進行標記清除的動作(有迴圈引用則各自-1)。

  But,原始碼內部在上述流程中提出了最佳化機制。

  5. Python快取機制

  5.1 程式碼塊&小資料池

  Python 程式碼塊、快取機制

  5.2 free_list

  快取機制

  一文搞定Python垃圾回收機制

  垃圾回收機制剖析


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69945560/viewspace-2693574/,如需轉載,請註明出處,否則將追究法律責任。

相關文章