1. 程式人生 > >Python之美[從菜鳥到高手]--Python垃圾回收機制及gc模組詳解

Python之美[從菜鳥到高手]--Python垃圾回收機制及gc模組詳解

    Python中的垃圾回收是以引用計數為主,標記-清除和分代收集為輔。引用計數最大缺陷就是迴圈引用的問題,所以Python採用了輔助方法。本篇文章並不詳細探討Python的垃圾回收機制的內部實現,而是以gc模組為切入點學習Python的垃圾回收機制,如果想深入可以讀讀<<Python原始碼剖析>>。

   看如下程式碼:

import gc
import sys
gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK)
a=[]
b=[]
a.append(b)
print 'a refcount:',sys.getrefcount(a)  # 2
print 'b refcount:',sys.getrefcount(b)  # 3

del a
del b
print gc.collect()  # 0

輸出結果:
a refcount: 2
b refcount: 3
gc: collecting generation 2...
gc: objects in each generation: 0 0 5131
gc: done, 0.0020s elapsed.
0
gc: collecting generation 2...
gc: objects in each generation: 0 0 5125
gc: done, 0.0010s elapsed.

    可以發現垃圾回收不起作用,所以垃圾收集只對迴圈引用起作用。

你可能好奇,為什麼a的引用數是2呢?這時候你需要去看看sys.getrefcount(object)的函式說明了?


哦,該函式Docstring中說返回值通常比我們期望的要多1,因為傳給該函式的引數臨時變數又增加了一次引用。原來是這樣,但讓人很奇怪的是,為啥不調整一下呢???

gc.collect()返回此次垃圾回收的unreachable(不可達)物件個數。那什麼是unreachable物件呢?請看下面一段程式碼:

a=[]
b=[]
a.append(b)
b.append(a)
del a
del b
print gc.collect()

輸出結果:
gc: collecting generation 2...
gc: objects in each generation: 4 0 5127
gc: collectable <list 02648918>
gc: collectable <list 026488A0>
gc: done, 2 unreachable, 0 uncollectable, 0.0030s elapsed.
2
    此次a,b是迴圈引用,垃圾回收果然起作用了,回收的兩個list的物件,就是a,b,不信可以使用:hex(id(a))輸出a的地址。

上面收集的兩個都是unreachable物件,那unreachable物件時什麼呢?在說明unreachable物件就需要了解Python的標記-清除垃圾回收機制了,簡單來說,過程如下:

** 尋找root object集合,root object多指全域性引用和函式棧上的引用,如上面程式碼所示,a就是root object


** 從root object出發,通過其每一個引用到達的所有物件都標記為reachable(垃圾檢測)


** 將所有非reachable的物件刪除(垃圾回收)


這裡還需要提到垃圾回收中的->>可收集物件連結串列,Python將所有可能產生迴圈引用的物件用連結串列連線起來,所謂的可產生迴圈引用的物件也就是list,dict,class等的容器類,int,string不是,每次例項化該種物件時都將加入這個連結串列,我們將該連結串列稱為可收集物件連結串列(ps該連結串列是雙向的)。

如,a=[],b=[],c={},將會產生:head <----> a  <----> b <----> c 雙向連結串列。

  我們可以假想上述程式碼的垃圾回收過程:當呼叫gc.collect()時,將從root object開始垃圾回收,由於del a ,del b後,a,b都將成為unreachable物件,且迴圈引用將被拆除,此時a,b引用數都是0,a,b將被回收,所以collect將返回2。

  看下面一段程式碼,將加深對上述的理解:

a=[]
b=[]
a.append(b)
b.append(a)
del b
print gc.collect()
輸出結果:
gc: collecting generation 2...
gc: objects in each generation: 354 4771 0
gc: done, 0.0010s elapsed.
0
gc: collecting generation 2...
gc: objects in each generation: 0 0 5119
gc: done, 0.0020s elapsed.
   此次並沒有垃圾回收,雖然del b了,但從a出發,找到了b的引用,所以b還是reachable物件,所以並不會被收集。

  Python有了垃圾回收機制是否意味著不會造成記憶體洩漏呢,非也,請看如下程式碼:

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

a=A()
b=B()
print hex(id(a))
print hex(id(a.__dict__))
a.b=b
b.a=a
del a
del b

print gc.collect()
print gc.garbage
輸出結果:
0x25cff30
0x25d0b70
gc: collecting generation 2...
gc: objects in each generation: 364 4771 0
gc: uncollectable <A instance at 025CFF30>
gc: uncollectable <B instance at 025CFF58>
gc: uncollectable <dict 025D0B70>
gc: uncollectable <dict 025D0810>
gc: done, 4 unreachable, 4 uncollectable, 0.0020s elapsed.
4
[<__main__.A instance at 0x025CFF30>, <__main__.B instance at 0x025CFF58>, {'b': <__main__.B instance at 0x025CFF58>}, {'a': <__main__.A instance at 0x025CFF30>}]
gc: collecting generation 2...
gc: objects in each generation: 2 0 5127
gc: done, 0.0010s elapsed.
   從輸出中我們看到uncollectable字樣,很明顯這次垃圾回收搞不定了,造成了記憶體洩漏。

為什麼會這樣呢?因為del b時,會呼叫b的__del__方法,該方法中很可能使用了b.a,但如果在之前的del a時將a給回收掉,此時將造成異常。所以Python沒辦法,造成了uncollectable,也就產生了記憶體洩漏。所以__del__方法要慎用,如果用的話一定要保證沒有迴圈引用。

   上面我們也打印出了a的地址,print hex(id(a)),也驗證了回收的的確是a。

   上面出現了gc.garbage,gc.garbage返回是unreachable物件,且不能被回收的的物件。仔細看看輸出結果,為什麼貌似有重複???這個困擾了我很久,直到開啟gc模組的文件才懂了。由於我們之前gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK),而gc.DEBUG_LEAK=gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS|gc.DEBUG_SAVEALL),文件中指出如果設定了gc.DEBUG_SAVEALL,那麼所有的unreachable物件都將加入gc.garbage返回的列表,而不止不能被回收的物件。

   我們看看Python的分代收集機制。

   Python中總共有三個“代”,所謂的三"代”就是三個連結串列,也就是我們上面所提到的可收集物件連結串列。當各個代中的物件數量達到一定數量時將觸發Python的垃圾回收,各個代的數量如下。


  分代收集的思想就是活的越久的物件,就越不是垃圾,回收的頻率就應該越低。所以當Python發現進過幾次垃圾回收該物件都是reachable,就將該物件移到二代中,以此類推。那麼Python中又是如何檢查各個代是否達到閥值的呢?Python中每次會從三代開始檢查,如果三代中的物件大於閥值將同時回收3,2,1代的物件。如果二代的滿足,將回收2,1代中的物件,設計的是如此的美。