【Python】 垃圾回收機制和gc模塊


垃圾回收機制和gc模塊

  Py的一個大好處,就是靈活的變量聲明和動態變量類型。雖然這使得學習py起來非常方便快捷,但是同時也帶來了py在性能上的一些不足。其中相關內存比較主要的一點就是py不會對已經銷毀的對象所占據的內存做自動的釋放內存空間的工作。

  在細看內存釋放工作之前,有必要先來了解一下py的垃圾回收機制。

■  垃圾回收機制

  Python中,主要依靠gc(garbage collector)模塊的引用計數技術來進行垃圾回收。所謂引用計數,就是考慮到Python中變量的本質不是內存中一塊存儲數據的區域,而是對一塊內存數據區域的引用。所以python可以給所有的對象(內存中的區域)維護一個引用計數的屬性,在一個引用被創建或復制的時候,讓python,把相關對象的引用計數+1;相反當引用被銷毀的時候就把相關對象的引用計數-1。當對象的引用計數減到0時,自然就可以認為整個python中不會再有變量引用這個對象,所以就可以把這個對象所占據的內存空間釋放出來了。

  引用計數技術在每次引用創建和銷毀時都要多做一些操作,這可能是一個小缺點,當創建和銷毀很頻繁的時候難免帶來一些效率上的不足。但是其最大的好處就是實時性,其他語言當中,垃圾回收可能只能在一些固定的時間點上進行,比如當內存分配失敗的時候進行垃圾回收,而引用計數技術可以動態地進行內存的管理。

  如果說效率只是一個不足的話,那么引用計數存在一些比較致命的軟肋使得其一直不被接受為一種可以廣泛運用的垃圾回收機制,這便是對循環引用的處理。在Python中有一些類型比如tuple,list,dict等,其作為容器類型可以包含若干個對象。如果某個對象就是它本身,或者兩個對象中互相包含對方,那么就構成了一個循環引用。比如下面這段代碼:

import sys
class Test():
  def __init__(self):
    pass

t = Test()
k = Test() t._self
= t print sys.getrefcount(t) #sys.getrefcount函數用來查看一個對象有幾個引用
print sys.getrefcount(k)
####結果####
3
2

  getrefcount函數查看一個對象存在幾個引用關系,一般狀態下的普通變量如上面的k,返回值都是2。不是1是因為把k作為參數傳遞給函數的時候,要先復制一份引用,然后把這個引用賦給形式參數供函數運行,在函數運行過程中,會保持這個引用始終升高為2。

  從上面運行的結果可以看出來,Test類實例t由於添加了一個自己對自己的引用,相當於:

這樣總共三個引用。

  del語句可以消除一個引用關系。對於沒有_self這樣的自我引用的情況下,del(k)相當於銷毀了變量名到內存地址的這一層引用關系,自getrefcount執行完成之后,這部分內存就可以得到釋放了。但是如果存在_self這個自我引用的話,即使消除了del(t)這個引用關系,這個對象的引用計數仍然是1。得不到銷毀,所以會造成內存泄露。

  可以看到,基於引用計數的垃圾回收機制因為循環引用的存在可能會導致內存泄露,所以python在引用計數的基礎上也增加了其他幾種垃圾回收的方式。這里簡單提一下。

  ●  標記-清除的回收機制

  針對循環引用這個問題,比如有兩個對象互相引用了對方,當外界沒有對他們有任何引用,也就是說他們各自的引用計數都只有1的時候,如果可以識別出這個循環引用,把它們屬於循環的計數減掉的話,就可以看到他們的真實引用計數了。基於這樣一種考慮,有一種方法,比如從對象A出發,沿着引用尋找到對象B,把對象B的引用計數減去1;然后沿着B對A的引用回到A,把A的引用計數減1,這樣就可以把這層循環引用關系給去掉了。

  不過這么做還有一個考慮不周的地方。假如A對B的引用是單向的, 在到達B之前我不知道B是否也引用了A,這樣子先給B減1的話就會使得B稱為不可達的對象了。為了解決這個問題,python中常常把內存塊一分為二,將一部分用於保存真的引用計數,另一部分拿來做為一個引用計數的副本,在這個副本上做一些實驗。比如在副本中維護兩張鏈表,一張里面放不可被回收的對象合集,另一張里面放被標記為可以被回收(計數經過上面所說的操作減為0)的對象,然后再到后者中找一些被前者表中一些對象直接或間接單向引用的對象,把這些移動到前面的表里面。這樣就可以讓不應該被回收的對象不會被回收,應該被回收的對象都被回收了。

  ●  分代回收

  分代回收策略着眼於提升垃圾回收的效率。研究表明,任何語言,任何環境的編程中,對於變量在內存中的創建/銷毀,總有頻繁和不那么頻繁的。比如任何程序中總有生命周期是全局的、部分的變量。

  而在垃圾回收的過程中,其實在進行垃圾回收之前還要進行一步垃圾檢測,即檢查某個對象是不是垃圾,該不該被回收。當對象很多,垃圾檢測將耗費大量的時間而真的垃圾回收花不了多久。對於這種多對象程序,我們可以把一些進行垃圾回收頻率相近的對象稱為“同一代”的對象。垃圾檢測的時候可以對頻率較高的“代”多檢測幾次,反之,進行垃圾回收頻率較低的“代”可以少檢測幾次。這樣就可以提高垃圾回收的效率了。至於如何判斷一個對象屬於什么代,python中采取的方法是通過其生存時間來判斷。如果在好幾次垃圾檢測中,該變量都是reachable的話,那就說明這個變量越不是垃圾,就要把這個變量往高的代移動,要減少對其進行垃圾檢測的頻率。

 

■  gc模塊的介紹

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

  最常見的gc模塊的使用就是用gc.collect()方法。那就先來看下這個方法把:

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方法也不是萬能的。有些時候它並不能有效地回收所有該回收的對象。比如下面這樣一段代碼:

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核心編程這本書在身邊的話還可以參閱一下,現在這些也就是一些網上碎片化信息的拼接。總之上面的話中可能有很多漏洞和錯誤,還是請看到這篇文章的人謹慎相信。】

 


免責聲明!

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



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