1. 程式人生 > >python進階(7)垃圾回收機制

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 ```   參考資料: