Python - 垃圾回收機制
引言:
直譯器在執行到定義變數的語法時,會申請記憶體空間來存放變數的值,而記憶體的容量是有限的,這就涉及到變數值所佔用記憶體空間的回收問題,當一個變數值沒有用了(簡稱垃圾)就應該將其佔用的記憶體給回收掉,那什麼樣的變數值是沒有用的呢?
單從邏輯層面分析,我們定義變數將變數值存起來的目的是為了以後取出來使用,而取得變數值需要通過其繫結的直接引用(如x=10,10被x直接引用)或間接引用(如l=[x,],x=10,10被x直接引用,而被容器型別l間接引用),所以當一個變數值不再繫結任何引用時,我們就無法再訪問到該變數值了,該變數值自然就是沒有用的,就應該被當成一個垃圾回收。
毫無疑問,記憶體空間的申請
與回收都是非常耗費精力的事情,而且存在很大的危險性,稍有不慎就有可能引發記憶體溢位問題,好在Cpython直譯器提供了自動的垃圾回收機制來幫我們解決了這件事。
一:簡介
1.什麼是垃圾
在python中,會定義一個變數名,然後賦給它變數值,如果這個變數呼叫結束了,不需要它了,將它刪除,但是這時候變數名被刪除了,變數值還在,這時候這個變數值就是 垃圾。
2.什麼是垃圾回收機制
垃圾回收機制(Garbage Collection)簡稱GC, 是python直譯器自帶的一種機制, 專門用來回收不可用的變數值所佔用的記憶體空間。
3.為什麼要有垃圾回收機制
程式在執行的過程中會申請記憶體空間, 而對於一些無用的記憶體空間如果不及時清理
但是記憶體管理是一件麻煩事, Python則提供了垃圾回收機制來釋放不必要的記憶體空間。
4.堆區 與 棧區
棧區:
用於存放 變數名與值記憶體地址的關聯關係。
堆區:
用於存放 變數值,也是記憶體管理回收的地方。
5.檢視變數的記憶體地址
x = 10 y = 20 # id(x) 可以檢視變數x的id print(id(x)) # 140716865820608 print(id(y)) # 140716865820928 # 用hex()把id(x)包起來,可以檢視變數x的記憶體地址 print(hex(id(x))) # 0x7ffb32cd17c0 print(hex(id(y))) # 0x7ffb32cd1900
二:引用計數
1.什麼是引用計數
Python垃圾回收主要以引用計數為主,分代回收為輔。引用計數法的原理是每個物件維護一個ob_ref,用來記錄當前物件被引用的次數,也就是來追蹤到底有多少引用指向了這個物件.
1個變數值的引用計數為0時,才會被當做垃圾回收。
x = 1 # 1的引用計數為1
y = 1 # 1的引用計數為2
z = 2 # 1的引用計數為3
z = y # 1的引用計數為2
print(hex(id(x))) # 0x7ffb24b016a0
print(hex(id(y))) # 0x7ffb24b016a0
print(hex(id(z))) # 0x7ffb24b016a0
引用計數增加的情況
1.物件被建立 a = 1
2.物件被引用 b = a
3.物件被作為引數,傳到函式中 func(a)
4.物件作為一個元素,儲存在容器中 l1 = {1, 'a', 'b',2}
引用計數減少的情況
1.當該物件的別名被顯式銷燬時 del a
2.當該物件的引別名被賦予新的物件, a = 30
3.一個物件離開它的作用域,例如 func函式執行完畢時,函式裡面的區域性變數的引用計數器就會減一(但是全域性變數不會)
4.將該元素從容器中刪除時,或者容器被銷燬時。
2.直接引用 與 間接引用
直接引用:
直接引用指的是從棧區出發直接引用到的記憶體地址。
x = 10
y = 20
z = 30
print(hex(id(x))) # 0x7ffb32cd17c0
print(hex(id(y))) # 0x7ffb32cd1900
print(hex(id(z))) # 0x7ffb2bec1a40
間接引用:
間接引用指的是從棧區出發引用到堆區後,再通過進一步引用才能到達的記憶體地址。
x = 10
y = x
l1 = [20, 30]
l2 = [x, l1]
print(hex(id(x))) # 0x7ffb32cd17c0
print(hex(id(y))) # 0x7ffb32cd17c0
print(hex(id(l1))) # 0x293b663fb80
print(hex(id(l2))) # 0x293b67868c0
迴圈引用 問題:
l1
和l2
互相引用
l1 = [1] # 此時l1被引用一次,引用計數為1
l2 = [2] # 此時l2被引用一次,引用計數為1
l1.append(l2) # l1=[值1的記憶體地址,l2列表的記憶體地址] #此時l2又被引用一次,引用計數為2
l2.append(l1) # l2=[值222的記憶體地址,l1列表的記憶體地址] #此時l1又被引用一次,引用計數為2
print(hex(id(l1[0]))) # 0x7ffb231416a0 # 1的記憶體地址
print(hex(id(l1[1]))) # 0x2be2d426900 # l2的記憶體地址 # l1引用l2
print(hex(id(l2))) # 0x2be2d426900
print(hex(id(l2[0]))) # 0x7ffb231416c0 # 2的記憶體地址
print(hex(id(l2[1]))) # 0x2be2d2dfb80 # l1的記憶體地址 # l2引用l1
print(hex(id(l1))) # 0x2be2d2dfb80
print(l2) # [2, [1, [...]]]
print(l1[1]) # [2, [1, [...]]]
# 此時,l1和l2互相引用
刪除l1
和l2
後,在堆區的l1和l2的記憶體地址中還存在著相互引用,使得它們的引用計數始終不為0。
這時候它們就無法被回收
,所以迴圈引用
是致命
的,這與手動進行記憶體管理所產生的記憶體洩露毫無區別。
所以Python引入了“標記-清除
” 與“分代回收
”來分別解決
引用計數的迴圈引用
與效率低
的問題
l1 = [1] # 此時l1被引用一次,引用計數為1
l2 = [2] # 此時l2被引用一次,引用計數為1
l1.append(l2) # l1=[值1的記憶體地址,l2列表的記憶體地址] #此時l2又被引用一次,引用計數為2
l2.append(l1) # l2=[值222的記憶體地址,l1列表的記憶體地址] #此時l1又被引用一次,引用計數為2
print(hex(id(l1[0]))) # 0x7ffb231416a0 # 1的記憶體地址
print(hex(id(l1[1]))) # 0x2be2d426900 # l2的記憶體地址 # l1引用l2
print(hex(id(l2))) # 0x2be2d426900
print(hex(id(l2[0]))) # 0x7ffb231416c0 # 2的記憶體地址
print(hex(id(l2[1]))) # 0x2be2d2dfb80 # l1的記憶體地址 # l2引用l1
print(hex(id(l1))) # 0x2be2d2dfb80
print(l2) # [2, [1, [...]]]
print(l1[1]) # [2, [1, [...]]]
# 此時,l1和l2互相引用
del l1 #l1引用次數-1
del l2 #l2引用次數-1
# 但是此時我們卻無法訪問l1/l2列表內的內容,本該被當做垃圾刪除.
# 但是它的引用計數為1,被l2/l1列表間接引用。
# 這個時候引用計數在這種垃圾身上就失效了。
三:標記 - 清除
容器物件
(比如:list,set,dict,class,instance)都可以包含對其他物件的引用
,所以都可能產生迴圈引用
。
而“標記-清除”計數就是為了解決迴圈引用
的問題。
標記/清除演算法的做法是當應用程式可用的記憶體空間被耗盡的時,就會停止整個程式,然後進行兩項工作,第一項則是標記,第二項則是清除
1.標記
通俗地講:
棧區相當於“根”,凡是從根出發可以訪達(直接或間接引用)的,都稱之為“有根之人”,有根之人當活,無根之人當死。
具體地:
標記的過程其實就是,遍歷所有的GC Roots物件(棧區中的所有內容或者執行緒都可以作為GC Roots物件),然後將所有GC Roots的物件可以直接或間接訪問到的物件標記為存活的物件,其餘的均為非存活物件,應該被清除。
2.清除
清除的過程將遍歷堆中所有的物件,將沒有標記的物件全部清除掉。
當有了標記 - 清除
機制之後,當同時刪除l1與l2時,會清理到棧區中l1與l2的內容以及直接引用關係
啟用標記清除演算法時,從棧區出發,沒有任何一條直接或間接引用可以訪達l1與l2,即l1與l2成了“無根之人”,於是l1與l2都沒有被標記為存活,二者會被清理掉,這樣就解決了迴圈引用帶來的記憶體洩漏問題。
四:分代回收
1.效率問題
基於引用計數的回收機制,每次回收記憶體,都需要把所有物件的引用計數都遍歷一遍,這是非常消耗時間的,於是引入了分代回收來提高回收效率,分代回收採用的是用“空間換時間”的策略。
2.分代
分代回收的核心思想是:
在歷經多次掃描的情況下,都沒有被回收的變數,gc機制就會認為,該變數是常用變數,gc對其掃描的頻率會降低,具體實現原理如下:
分代指的是根據存活時間來為變數劃分不同等級(也就是不同的代)
新定義的變數,放到新生代這個等級中,假設每隔1分鐘掃描新生代一次,如果發現變數依然被引用,那麼該物件的權重(權重本質就是個整數)加一。
當變數的權重大於某個設定得值(假設為3),會將它移動到更高一級的青春代,青春代的gc掃描的頻率低於新生代(掃描時間間隔更長),假設5分鐘掃描青春代一次。
這樣每次gc需要掃描的變數的總個數就變少了,節省了掃描的總時間,接下來,青春代中的物件,也會以同樣的方式被移動到老年代中。
也就是等級(代)越高,被垃圾回收機制掃描的頻率越低
3.回收
回收依然是使用引用計數作為回收的依據
以班級為例,可以把學生分為三類:學霸、普通學生、學渣
學霸:過於優秀,一週查一次作業
普通學生:一般般,三天查一次
學渣:不用多說,每天都查
雖然分級檢查了,檢查的時間效率變高了,但是存在著缺陷,如果學霸不交作業,要下一週才能 查出來,那他的學霸地位就不保了,存在漏洞。
沒有十全十美的方案:
毫無疑問,如果沒有分代回收,即引用計數機制一直不停地對所有變數進行全體掃描,可以更及時地清理掉垃圾佔用的記憶體,但這種一直不停地對所有變數進行全體掃描的方式效率極低,所以我們只能將二者中和。
綜上
垃圾回收機制是在清理垃圾&釋放記憶體的大背景下,允許分代回收以極小部分垃圾不會被及時釋放為代價,以此換取引用計數整體掃描頻率的降低,從而提升其效能,這是一種以空間換時間的解決方案目錄。