python進階19垃圾回收GC

原創博客鏈接:python進階19垃圾回收GC

垃圾收集三大手段

一、引用計數(計數器)

Python垃圾回收主要以引用計數爲主,分代回收爲輔。引用計數法的原理是每個對象維護一個ob_ref,用來記錄當前對象被引用的次數,也就是來追蹤到底有多少引用指向了這個對象,當發生以下四種情況的時候,該對象的引用計數器+1

 

1
2
3
4
對象被創建  a=14
對象被引用  b=a
對象被作爲參數,傳到函數中   func(a)
對象作爲一個元素,存儲在容器中   List={a,”a”,”b”,2}

與上述情況相對應,當發生以下四種情況時,該對象的引用計數器-1

 

1
2
3
4
當該對象的別名被顯式銷燬時  del a
當該對象的引別名被賦予新的對象,   a=26
一個對象離開它的作用域,例如 func函數執行完畢時,函數裏面的局部變量的引用計數器就會減一(但是全局變量不會)
將該元素從容器中刪除時,或者容器被銷燬時。

.當指向該對象的內存的引用計數器爲0的時候,該內存將會被Python虛擬機銷燬
優點:

 

1
2
3
4
高效
運行期沒有停頓 可以類比一下Ruby的垃圾回收機制,也就是 實時性:一旦沒有引用,內存就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。
對象有確定的生命週期
易於實現

原始的引用計數法也有明顯的缺點:

 

1
2
維護引用計數消耗資源,維護引用計數的次數和引用賦值成正比,而不像mark and sweep等基本與回收的內存數量有關。
無法解決循環引用的問題。A和B相互引用而再沒有外部引用A與B中的任何一個,它們的引用計數都爲1,但顯然應該被回收。

二、標記-清除(雙向鏈表)

『標記清除(Mark—Sweep)』算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收算法。它分爲兩個階段:第一階段是標記階段,GC會把所有的『活動對象』打上標記,第二階段是把那些沒有標記的對象『非活動對象』進行回收。
那麼GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?
對象之間通過引用(指針)連在一起,構成一個有向圖,對象構成這個有向圖的節點,而引用關係構成這個有向圖的邊。從根對象(root object)出發,沿着有向邊遍歷對象,可達的(reachable)對象標記爲活動對象,不可達的對象就是要被清除的非活動對象。根對象就是全局變量、調用棧、寄存器。

在上圖中,我們把小黑圈視爲全局變量,也就是把它作爲root object,從小黑圈出發,對象1可直達,那麼它將被標記,對象2、3可間接到達也會被標記,而4和5不可達,那麼1、2、3就是活動對象,4和5是非活動對象會被GC回收。

 

1
標記清除算法作爲Python的輔助垃圾收集技術主要處理的是一些容器對象,比如list、dict、tuple,instance等,因爲對於字符串、數值對象是不可能造成循環引用問題。Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內存,哪怕只剩下小部分活動對象也要掃描所有對象。

檢測循環引用
隨後,Python會循環遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據規則減掉其引用計數。在這個過程中,Python會一個接一個的統計內部引用的數量以防過早地釋放對象。

大多數情況下,循環引用計數其實都是>1的,所以-1後其實也>0,因爲其大概率不止有一個指針,可能會指向多個地址,指向的多個地址中~有一個地址被循環引用外的元素引用(也即是說此元素是真正被需要的,非互相循環導致的假需求),就不會導致計數器爲0

三、分代回收

爲了便於理解,來看一個例子:

從而被分配對象的計數值與被釋放對象的計數值之間的差異在逐漸增長。一旦這個差異累計超過某個閾值(說白了就是0代留存量超過閾值,0代鏈表長度超過閾值),則Python的收集機制就啓動了,並且觸發上邊所說到的零代算法,釋放“浮動的垃圾”,並且將剩下的對象移動到一代列表。
而Python對於一代列表中對象的處理遵循同樣的方法,一旦被分配計數值與被釋放計數值累計到達一定閾值,Python會將剩下的活躍對象移動到二代列表。
通過不同的閾值設置,Python可以在不同的時間間隔處理這些對象。Python處理零代最爲頻繁,其次是一代然後纔是二代。

垃圾收集何時進行?

del 僅在參考計數達到0時執行

爲什麼定義了del的循環引用對象在Python中無法收集

從gc.garbage的文檔中:
Python不會自動收集此類循環,因爲通常來說,Python不可能猜測出運行del()方法的安全順序 。如果您知道安全訂單,則可以通過檢查垃圾清單並由於清單中的對象而明確中斷週期來強制執行此問題。

簡單來說,gc會計算出循環引用的計數器=0,所以會嘗試回收,但是由於自定義了del方法(重寫了obj的del),所以就會懵逼,不知道從循環的哪裏下口。除非必要,否則別重寫del,至於del中只有pass的就更沒必要了.

代碼測試

我們知道了python對於垃圾回收,採取的是引用計數爲主,標記-清除+分代回收爲輔的回收策略。對於循環引用的情況,一般的自動垃圾回收方式肯定是無效了,這時候就需要顯式地調用一些操作來保證垃圾的回收和內存不泄露。這就要用到python內建的垃圾回收模塊gc模塊了。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import gc

a = [1]
b = [2]
a.append(b)
b.append(a)
####此時a和b之間存在循環引用####
sys.getrefcount(a)    #結果應該是3
sys.getrefcount(b)    #結果應該是3
del a
del b
####刪除了變量名a,b到對象的引用,此時引用計數應該減爲1,即只剩下互相引用了####
try:
    sys.getrefcount(a)
except UnboundLocalError:
     print 'a is invalid'
####此時,原來a指向的那個對象引用不爲0,python不會自動回收它的內存空間####
####但是我們又沒辦法通過變量名a來引用它了,這就導致了內存泄露####
unreachable_count = gc.collect()
####gc.collect()專門用來處理這些循環引用,返回處理這些循環引用一共釋放掉的對象個數。這裏返回是2####

可以看到,沒有gc模塊的時候,我們對循環引用是束手無策的,在調用了一些gc模塊的方法之後,它會實現上面“垃圾回收機制”部分中提到的一些策略比如“標記-清除”來進行垃圾回收。因爲有了這個模塊的封裝,我們就不用關心具體的實現了。

然而collect方法也不是萬能的。有些時候它並不能有效地回收所有該回收的對象。比如下面這樣一段代碼:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A():
  def __init__(self):
    pass
  def __del__(self):
    pass

class B():
  def __init__(self):
    pass
  def __del__(self):
    pass

a = A()
b = B()
a._b = b
b._a = a
del a
del b

print gc.collect()    #結果是4
print gc.garbage    #結果是[<__main__.A instance at 0x0000000002296448>, <__main__.B instance at 0x0000000002296488>]

可以看到,對我們自定義類的對象而言,collect方法並不能解決循環引用引起的內存泄露,即使在collect過後,解釋器中仍然存在兩個垃圾對象。
這裏需要明確一下,之前對於“垃圾”二字的定義並不是很明確,在這裏的這個語境下,垃圾是指在經過collect的垃圾回收之後仍然保持unreachable狀態,即無法被回收,且無法被用戶調用的對象應該叫做垃圾。gc模塊中有garbage這個屬性,其爲一個列表,每一項都是當前解釋器中存在的垃圾對象。一般情況下,這個屬性始終保持爲空集。
那麼爲什麼在這種場景下collect不起作用了呢?這主要是因爲我們在類中重載了del方法。del方法指出了在用del語句刪除對象時除了釋放內存空間以外的操作。一般而言,在使用了del語句的時候解釋器會首先看要刪除對象的引用計數,如果爲0,那麼就釋放內存並執行del方法。在這裏,首先del語句出現時本身引用計數就不爲0(因爲有循環引用的存在),所以解釋器不釋放內存;再者,執行collect方法時照理由應該會清除循環引用所產生的無效引用計數從而達到del的目的,對於這兩個對象而言,python無法判斷調用它們的del方法時會不會要用到對方那個對象,比如在進行b._ del_ ()時可能會用到b.a也就是a,如果在那之前a已經被釋放,那麼就徹底GG了。爲了避免這種情況,collect方法默認不對重載了del方法的循環引用對象進行回收,而它們倆的狀態也會從unreachable轉變爲uncollectable。由於是uncollectable的,自然就不會被collect處理,所以就進入了garbage列表。
collect返回4的原因是因爲,在A和B類對象中還默認有一個dict屬性,裏面有所有屬性的信息。比如對於a,有a. _
 dict __ = {‘b’:< _main .B instance at xxxxxxxx>}。a的dict和b的dict也是循環引用的。但是字典類型不涉及自定義的del方法,所以可以被collect掉。所以garbage裏只剩下兩個了。
有時候garbage裏也會出現那兩個dict,這主要是因爲在前面可能設置了gc模塊的debug模式,比如gc.set_debug(gc.DEBUG_LEAK),會把所有已經回收掉的unreachable的對象也都加入到garbage裏面。set_debug還有很多參數諸如gc.DEBUG_STAT|DEBUG_COLLECTABLE|DEBUG_UNCOLLECTABLE|DEBUG_SAVEALL等等,設置了相關參數後gc模塊會自動檢測垃圾回收狀況並給出實時地信息反映。

gc.get_threshold()
這個方法涉及到之前說過的分代回收的策略。python中默認把所有對象分成三代。第0代包含了最新的對象,第2代則是最早的一些對象。在一次垃圾回收中,所有未被回收的對象會被移到高一代的地方。
這個方法返回的是(700,10,10),這也是gc的默認值。這個值的意思是說,在第0代對象數量達到700個之前,不把未被回收的對象放入第一代;而在第一代對象數量達到10個之前也不把未被回收的對象移到第二代。可以是使用gc.set_threshold(threashold0,threshold1,threshold2)來手動設置這組閾值。

參考

【Python】 垃圾回收機制和gc模塊:https://www.cnblogs.com/franknihao/p/7326849.html
Python垃圾回收機制詳解:https://blog.csdn.net/xiongchengluo1129/article/details/80462651
del的幾個坑:https://blog.csdn.net/pirDOL/article/details/51586406
涉及循環引用時del方法未執行:https://www.pythonheidong.com/blog/article/398409/
爲什麼定義了del的循環引用對象在Python中無法收集?:https://www.pythonheidong.com/blog/article/141888/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章