python進階(7)垃圾回收機制

Python垃圾回收

基於C語言源碼底層,讓你真正了解垃圾回收機制的實現

  • 引用計數器
  • 標記清除
  • 分代回收
  • 快取機制
  • Python的C源碼(3.8.2版本)
     

1.引用計數器

 

1.1環狀雙向鏈表 refchain

在python中創建的任何對象都會放在refchain鏈表中

name = 'jack'
age = 18
hobby = ['籃球', '美女']
內部會創建一些數據【上一個對象、下一個對象、類型、引用個數】
name = 'jack'
new = name

內部會創建一些數據【上一個對象、下一個對象、類型、引用個數、val=18】
age = 18

內部會創建一些數據【上一個對象、下一個對象、類型、引用個數、items=元素、元素個數】
hobby = ['籃球', '美女']

在C源碼中如何體現每個對象都有的相同的值:PyObject結構體(4個值)
有多個元素組成的對象:Pyobject結構體(4個值)+ ob_size
 

1.2類型封裝結構體

data = 3.14

內部會創建:
        _ob_next = refchain中的上一個對象
    _ob_prev = refchain中的下一個對象
    ob_refcnt = 1
    ob_type = float
    ob_fval = 3.14

 

1.3引用計數器

v1 = 3.14
v2 = 999
v3 = (1, 2, 3)

當python程式運行時,會根據數據結構的不同來找到對應的結構體,根據結構體中的欄位來進行創建相關的數據,然後將對象添加到refchain雙向鏈表中。
在C源碼中有兩個關鍵的結構體:PyObject、PyVarObject
每個對象中有ob_refcnt就是引用計數器,值默認為1,當有其他變數引用對象時,引用計數器就會發生變化。

  • 引用
a = 9999
b = a
  • 刪除引用
a = 9999
b = a 
del b  # b變數刪除,b對應對象引用計數器-1
del a  # a變數刪除,a對應對象引用計數器-1

# 當一個對象引用的計數器為0時,意味著沒有人再使用這個對象了,這個對象就是垃圾,需要被回收
# 回收:1.對象從refchain鏈表中移除,2.將對象銷毀,記憶體歸還

 

1.4循環引用的問題

基於引用計數器進行垃圾回收非常方便和簡單,但他還是存在循環引用的問題,導致無法正常的回收一些數據,例如:

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

 

2.標記清除

對於上述程式碼會發現,執行del操作之後,沒有變數再會去使用那兩個列表對象,但由於循環引用的問題,他們的引用計數器不為0,所以他們的狀態:永遠不會被使用、也不會被銷毀。項目中如果這種程式碼太多,就會導致記憶體一直被消耗,直到記憶體被耗盡,程式崩潰。
 
為了解決循環引用的問題,引入了標記清除技術,專門針對那些可能存在循環引用的對象進行特殊處理,可能存在循環應用的類型有:列表、元組、字典、集合、自定義類等那些能進行數據嵌套的類型。
 
標記清除:創建特殊鏈表專門用於保存 列表、元組、字典、集合、自定義類等對象,之後再去檢查這個鏈表中的對象是否存在循環引用,如果存在則讓雙方的引用計數器均 – 1 。如果減完為0,則垃圾回收
 

3.分代回收

對標記清除中的鏈表進行優化,將那些可能存在循引用的對象拆分到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代和1、2代的threshold和count表示的意義不同。

  • 0代,count表示0代鏈表中對象的數量,threshold表示0代鏈表對象個數閾值,超過則執行一次0代掃描檢查
  • 1代,count表示0代鏈表掃描的次數,threshold表示0代鏈表掃描的次數閾值,超過則執行一次1代掃描檢查。
  • 2代,count表示1代鏈表掃描的次數,threshold表示1代鏈表掃描的次數閾值,超過則執行一2代掃描檢查。
     

4.小結

在python中維護了一個refchain雙向環狀鏈表、這個鏈表中存儲程式創建的所有對象,每種類型的對象中都有一個ob_refcnt引用計數器的值,引用個數+1、-1,最後當引用計數器變為0時會進行垃圾回收(對象銷毀、refchain中移除)。
 
但是,python中那些可以有多個元素組成的對象可能會存在出現循環引用的問題,為了解決這個問題,python又引入了標記清除和分代回收,在其內部為4個鏈表

  • refchain
  • 2代,10次
  • 1代,10次
  • 0代,700次

在源碼內部當達到各自的閾值時,會出發掃描鏈表進行標記清除的動作(有循環就各自-1),但是源碼內部還提供了優化機制
 

5.Python快取

從上文大家可以了解到當對象的引用計數器為0時,就會被銷毀並釋放記憶體。而實際上他不是這麼的簡單粗暴,因為反覆的創建和銷毀會使程式的執行效率變低。Python中引入了「快取機制」機制。 例如:引用計數器為0時,不會真正銷毀對象,而是將他放到一個名為 free_list 的鏈表中,之後會再創建對象時不會在重新開闢記憶體,而是在free_list中將之前的對象來並重置內部的值來使用。

  • float類型,維護的free_list鏈表最多可快取100個float對象。
  v1 = 3.14    # 開闢記憶體來存儲float對象,並將對象添加到refchain鏈表。
  print( id(v1) ) # 記憶體地址:140599203433232
  del v1    # 引用計數器-1,如果為0則在rechain鏈表中移除,不銷毀對象,而是將對象添加到float的free_list.
  v2 = 9.999    # 優先去free_list中獲取對象,並重置為9.999,如果free_list為空才重新開闢記憶體。
  print( id(v2) ) # 記憶體地址:140599203433232
  # 注意:引用計數器為0時,會先判斷free_list中快取個數是否滿了,未滿則將對象快取,已滿則直接將對象銷毀。
  • int類型,不是基於free_list,而是維護一個small_ints鏈表保存常見數據(小數據池),小數據池範圍:-5 <= value < 257。即:重複使用這個範圍的整數時,不會重新開闢記憶體。
  v1 = 38    # 去小數據池small_ints中獲取38整數對象,將對象添加到refchain並讓引用計數器+1。
  print( id(v1))  #記憶體地址:4401668032
  v2 = 38 # 去小數據池small_ints中獲取38整數對象,將refchain中的對象的引用計數器+1。
  print( id(v2) ) #記憶體地址:4401668032
  # 注意:在解釋器啟動時候-5~256就已經被加入到small_ints鏈表中且引用計數器初始化為1,程式碼中使用的值時直接去small_ints中拿來用並將引用計數器+1即可。另外,small_ints中的數據引用計數器永遠不會為0(初始化時就設置為1了),所以也不會被銷毀。
  • str類型,維護unicode_latin1[256]鏈表,內部將所有的ascii字元快取起來,以後使用時就不再反覆創建。
  v1 = "A"
  print( id(v1) ) # 輸出:140599159374000
  del v1
  v2 = "A"
  print( id(v1) ) # 輸出:140599159374000
  # 除此之外,Python內部還對字元串做了駐留機制,針對那麼只含有字母、數字、下劃線的字元串(見源碼Objects/codeobject.c),如果記憶體中已存在則不會重新在創建而是使用原來的地址里(不會像free_list那樣一直在記憶體存活,只有記憶體中有才能被重複利用)。
  v1 = "jack"
  v2 = "jack"
  print(id(v1) == id(v2)) # 輸出:True
  • list類型,維護的free_list數組最多可快取80個list對象。
  v1 = [11,22,33]  
  print( id(v1) ) # 輸出:4517628816
  del v1
  v2 = ["j","ack"]
  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 = ("甲殼蟲","Alex")  # 不會重新開闢記憶體,而是去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(v2) ) # 輸出:4515998128

 
參考資料://pythonav.com/wiki/detail/6/88/