python垃圾回收和緩存管理


Python垃圾回收和緩存管理

你有沒有想過為什么我們頻繁地使用Python敲代碼做項目,實際上一直在生產對象並不斷占用內存,而我們很少會去清理Python的內存,理論上來講它總有一天把內存消耗殆盡(溢出),可每次打開Python卻“安然無恙”?真的只是你的計算機內存很大嗎?

並不是,一個成熟的軟件它都會有自己的內存管理和垃圾回收機制,而不是光靠硬件來提供絕對支持。

img

Python也是有它的垃圾回收機制,這也是面試的時候面試官喜歡問的一個問題:Python的內存管理和垃圾回收機制的原理是什么?

很多時候我們太過於注重一些表層的東西而忽視了里層。

就好比我們開車,如果你只知道加油、插鑰匙、踩油門、剎車和方向盤等這些表面操作,對發動機艙的東西一無所知,你算不得老司機,早晚你得在大馬路上過夜。

今天就來跟大家講講Python的內存管理和垃圾回收機制是怎么一個原理,更加深入地了解Python,避免下次被問到這種問題你回答不上來。

一、大管家refchain

在Python的C源碼中有一個名為refchain的環狀雙向鏈表,這個鏈表比較牛逼了,因為Python程序中一旦創建對象都會把這個對象添加到refchain這個鏈表中。也就是說他保存着所有的對象。例如:

age = 18 
name = "張三"

img

二、引用計數器

在refchain中的所有對象內部都有一個ob_refcnt用來保存當前對象的引用計數器,顧名思義就是自己被引用的次數,例如:

age = 18 
name = "張三" 
nickname = name

上述代碼表示內存中有 18 和 “張三” 兩個值,他們的引用計數器分別為:1、2 。

img

當值被多次引用時候,不會在內存中重復創建數據,而是引用計數器+1 。 當對象被銷毀時候同時會讓引用計數器-1,如果引用計數器為0,則將對象從refchain鏈表中摘除,同時在內存中進行銷毀(暫不考慮緩存等特殊情況)。

age = 18 
number = age # 對象18的引用計數器 + 1 
del age # 對象18的引用計數器 - 1 
def run(arg): 
	print(arg) 
run(number) # 剛開始執行函數時,對象18引用計數器 + 1,當函數執行完畢之后,對象18引用計數器 - 1 。 
num_list = [11,22,number] # 對象18的引用計數器 + 1

img

三、 標記清除&分代回收

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

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追加到v2中,則v1對應的[11,22,33]對象的引用計數器加1,最終為2. 
del v1 # 引用計數器-1 
del v2 # 引用計數器-1

對於上述代碼會發現,執行del操作之后,沒有變量再會去使用那兩個列表對象,但由於循環引用的問題,他們的引用計數器不為0,所以他們的狀態:永遠不會被使用、也不會被銷毀。項目中如果這種代碼太多,就會導致內存一直被消耗,直到內存被耗盡,程序崩潰。

為了解決循環引用的問題,引入了標記清除技術,專門針對那些可能存在循環引用的對象進行特殊處理,可能存在循環應用的類型有:列表、元組、字典、集合、自定義類等那些能進行數據嵌套的類型。

標記清除:創建特殊鏈表專門用於保存 列表、元組、字典、集合、自定義類等對象,之后再去檢查這個鏈表中的對象是否存在循環引用,如果存在則讓雙方的引用計數器均 - 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代和1、2代的threshold和count表示的意義不同。

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

1代,count表示0代鏈表掃描的次數,threshold表示0代鏈表掃描的次數閾值,超過則執行一次1代掃描檢查。

2代,count表示1代鏈表掃描的次數,threshold表示1代鏈表掃描的次數閾值,超過則執行一2代掃描檢查。

四、 情景模擬

根據C語言底層並結合圖來講解內存管理和垃圾回收的詳細過程。

第一步:當創建對象age=19時,會將對象添加到refchain鏈表中。

第二步:當創建對象num_list = [11,22]時,會將列表對象添加到 refchain 和 generations 0代中。

第三步:新創建對象使generations的0代鏈表上的對象數量大於閾值700時,要對鏈表上的對象進行掃描檢查

當0代大於閾值后,底層不是直接掃描0代,而是先判斷2、1是否也超過了閾值

如果2、1代未達到閾值,則掃描0代,並讓1代的 count + 1

如果2代已達到閾值,則將2、1、0三個鏈表拼接起來進行全掃描,並將2、1、0代的count重置為0

如果1代已達到閾值,則講1、0兩個鏈表拼接起來進行掃描,並將所有1、0代的count重置為0

對拼接起來的鏈表在進行掃描時,主要就是剔除循環引用和銷毀垃圾,詳細過程為:

掃描鏈表,把每個對象的引用計數器拷貝一份並保存到 gc_refs中,保護原引用計數器。

再次掃描鏈表中的每個對象,並檢查是否存在循環引用,如果存在則讓各自的gc_refs減 1

再次掃描鏈表,將 gc_refs 為 0 的對象移動到unreachable鏈表中;不為0的對象直接升級到下一代鏈表中

處理unreachable鏈表中的對象的 析構函數 和 弱引用,不能被銷毀的對象升級到下一代鏈表,能銷毀的保留在此鏈表

析構函數,指的就是那些定義了__del__方法的對象,需要執行之后再進行銷毀處理

弱引用

最后將 unreachable 中的每個對象銷毀並在refchain鏈表中移除(不考慮緩存機制)

至此,垃圾回收的過程結束。

五、 緩存機制

從上文大家可以了解到當對象的引用計數器為0時,就會被銷毀並釋放內存。而實際上他不是這么的簡單粗暴,因為反復的創建和銷毀會使程序的執行效率變低。

Python中引入了“緩存機制”。

例如:引用計數器為0時,不會真正銷毀對象,而是將他放到一個名為 free_list 的鏈表中,之后會再創建對象時不會在重新開辟內存,而是在free_list中將之前的對象來並重置內部的值來使用。

float類型,維護的free_list鏈表最多可緩存100個float對象。

v1 = 3.14 # 開辟內存來存儲float對象,並將對象添加到refchain鏈表。 
print(id(v1)) 
# 內存地址:4436033488 
del v1 # 引用計數器-1,如果為0則在rechain鏈表中移除,不銷毀對象,而是將對象添加到float的free_list. 
v2 = 9.999 # 優先去free_list中獲取對象,並重置為9.999,如果free_list為空才重新開辟內存。 
print(id(v2)) 
# 內存地址:4436033488 
# 注意:引用計數器為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 = ("張三","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(v1) ) # 輸出:4515998128


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM