使用gc、objgraph幹掉python內存泄露與循環引用!

  Python使用引用計數和垃圾回收來作內存管理,前面也寫過一遍文章《Python內存優化》,介紹了在python中,如何profile內存使用狀況,並作出相應的優化。本文介紹兩個更致命的問題:內存泄露與循環引用。內存泄露是讓全部程序員都聞風喪膽的問題,輕則致使程序運行速度減慢,重則致使程序崩潰;而循環引用是使用了引用計數的數據結構、編程語言都須要解決的問題。本文揭曉這兩個問題在python語言中是如何存在的,而後試圖利用gc模塊和objgraph來解決這兩個問題。html

  注意:本文的目標是Cpython,測試代碼都是運行在Python2.7。另外,本文不考慮C擴展形成的內存泄露,這是另外一個複雜且頭疼的問題。node

    本文地址:http://www.cnblogs.com/xybaby/p/7491656.htmlpython

一分鐘版本

  (1)python使用引用計數和垃圾回收來釋放(free)Python對象git

  (2)引用計數的優勢是原理簡單、將消耗均攤到運行時;缺點是沒法處理循環引用程序員

  (3)Python垃圾回收用於處理循環引用,可是沒法處理循環引用中的對象定義了__del__的狀況,並且每次回收會形成必定的卡頓github

  (4)gc module是python垃圾回收機制的接口模塊,能夠經過該module啓停垃圾回收、調整回收觸發的閾值、設置調試選項
 
  (5)若是沒有禁用垃圾回收,那麼Python中的內存泄露有兩種狀況:要麼是對象被生命週期更長的對象所引用,好比global做用域對象;要麼是循環引用中存在__del__
  (6)使用gc module、objgraph能夠定位內存泄露,定位以後,解決很簡單
 
  (7)垃圾回收比較耗時,所以在對性能和內存比較敏感的場景也是沒法接受的,若是能解除循環引用,就能夠禁用垃圾回收。
  (8)使用gc module的DEBUG選項能夠很方便的定位循環引用,解除循環引用的辦法要麼是手動解除,要麼是使用weakref

python內存管理

  Python中,一切都是對象,又分爲mutable和immutable對象。兩者區分的標準在因而否能夠原地修改,「原地「」能夠理解爲相同的地址。能夠經過id()查看一個對象的「地址」,若是經過變量修改對象的值,但id沒發生變化,那麼就是mutable,不然就是immutable。好比:web

>>> a = 5;id(a)算法

35170056
>>> a = 6;id(a)
35170044
>>> lst = [1,2,3]; id(lst)
39117168
>>> lst.append(4); id(lst)
39117168

  a指向的對象(int類型)就是immutable, 賦值語句只是讓變量a指向了一個新的對象,由於id發生了變化。而lst指向的對象(list類型)爲可變對象,經過方法(append)能夠修改對象的值,同時保證id一致。編程

  判斷兩個變量是否相等(值相同)使用==, 而判斷兩個變量是否指向同一個對象使用 is。好比下面a1 a2這兩個變量指向的都是空的列表,值相同,可是不是同一個對象。緩存

>>> a1, a2 = [], []
>>> a1 == a2
True
>>> a1 is a2
False

  

  爲了不頻繁的申請、釋放內存,避免大量使用的小對象的構造析構,python有一套本身的內存管理機制。在鉅著《Python源碼剖析》中有詳細介紹,在python源碼obmalloc.h中也有詳細的描述。以下所示:

  

  能夠看到,python會有本身的內存緩衝池(layer2)以及對象緩衝池(layer3)。在Linux上運行過Python服務器的程序都知道,python不會當即將釋放的內存歸還給操做系統,這就是內存緩衝池的緣由。而對於可能被常用、並且是immutable的對象,好比較小的整數、長度較短的字符串,python會緩存在layer3,避免頻繁建立和銷燬。例如:

>>> a, b = 1, 1
>>> a is b
True
>>> a, b = (), ()
>>> a is b
True
>>> a, b = {}, {}
>>> a is b
False

  本文並不關心python是如何管理內存塊、如何管理小對象,感興趣的讀者能夠參考伯樂在線csdn上的這兩篇文章。

  本文關心的是,一個普通的對象的生命週期,更明確的說,對象是何時被釋放的。當一個對象理論上(或者邏輯上)再也不被使用了,但事實上沒有被釋放,那麼就存在內存泄露;當一個對象事實上已經不可達(unreachable),即不能經過任何變量找到這個對象,但這個對象沒有當即被釋放,那麼則可能存在循環引用。

引用計數

  引用計數(References count),指的是每一個Python對象都有一個計數器,記錄着當前有多少個變量指向這個對象。

  將一個對象直接或者間接賦值給一個變量時,對象的計數器會加1;當變量被del刪除,或者離開變量所在做用域時,對象的引用計數器會減1。當計數器歸零的時候,表明這個對象再也沒有地方可能使用了,所以能夠將對象安全的銷燬。Python源碼中,經過Py_INCREF和Py_DECREF兩個宏來管理對象的引用計數,代碼在object.h

 1 #define Py_INCREF(op) (                         \
 2     _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
 3     ((PyObject*)(op))->ob_refcnt++)
 4 
 5 #define Py_DECREF(op)                                   \
 6     do {                                                \
 7         if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
 8         --((PyObject*)(op))->ob_refcnt != 0)            \
 9             _Py_CHECK_REFCNT(op)                        \
10         else                                            \
11         _Py_Dealloc((PyObject *)(op));                  \
12     } while (0)

   經過sys.getrefcount(obj)對象能夠得到一個對象的引用數目,返回值是真實引用數目加1(加1的緣由是obj被當作參數傳入了getrefcount函數),例如:

>>> import sys
>>> s = 'asdf'
>>> sys.getrefcount(s)
2
>>> a = 1
>>> sys.getrefcount(a)
605

  從對象1的引用計數信息也能夠看到,python的對象緩衝池會緩存十分經常使用的immutable對象,好比這裏的整數1。

  引用計數的優勢在於原理通俗易懂;且將對象的回收分佈在代碼運行時:一旦對象再也不被引用,就會被釋放掉(be freed),不會形成卡頓。但也有缺點:額外的字段(ob_refcnt);頻繁的加減ob_refcnt,並且可能形成連鎖反應。但這些缺點跟循環引用比起來都不算事兒。

 

  什麼是循環引用,就是一個對象直接或者間接引用本身自己,引用鏈造成一個環。且看下面的例子:

 1 # -*- coding: utf-8 -*-
 2 import objgraph, sys
 3 class OBJ(object):
 4     pass
 5 
 6 def show_direct_cycle_reference():
 7     a = OBJ()
 8     a.attr = a
 9     objgraph.show_backrefs(a, max_depth=5, filename = "direct.dot")
10 
11 def show_indirect_cycle_reference():
12     a, b = OBJ(), OBJ()
13     a.attr_b = b
14     b.attr_a = a
15     objgraph.show_backrefs(a, max_depth=5, filename = "indirect.dot")
16 
17 if __name__ == '__main__':
18     if len(sys.argv) > 1:
19         show_direct_cycle_reference()
20     else:
21         show_indirect_cycle_reference()
循環引用示例

  運行上面的代碼,使用graphviz工具集(本文使用的是dotty)打開生成的兩個文件,direct.dot 和 indirect.dot,獲得下面兩個圖

      

  經過屬性名(attr, attr_a, attr_b)能夠很清晰的看出循環引用是怎麼產生的

  前面已經提到,對於一個對象,當沒有任何變量指向本身時,引用計數降到0,就會被釋放掉。咱們以上面左邊那個圖爲例,能夠看到,紅框裏面的OBJ對象想在有兩個引用(兩個入度),分別來自幀對象frame(代碼中,函數局部空間持有對OBJ實例的引用)、attr變量。咱們再改一下代碼,在函數運行技術以後看看是否還有OBJ類的實例存在,引用關係是怎麼樣的:

 1 # -*- coding: utf-8 -*-
 2 import objgraph, sys
 3 class OBJ(object):
 4     pass
 5 
 6 def direct_cycle_reference():
 7     a = OBJ()
 8     a.attr = a
 9     
10 if __name__ == '__main__':
11     direct_cycle_reference()
12     objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth=5, filename = "direct.dot"
循環引用示例2

  

  修改後的代碼,OBJ實例(a)存在於函數的local做用域。所以,當函數調用結束以後,來自幀對象frame的引用被解除。從圖中能夠看到,當前對象的計數器(入度)爲1,按照引用計數的原理,是不該該被釋放的,但這個對象在函數調用結束以後就是事實上的垃圾,這個時候就須要另外的機制來處理這種狀況了。

  python的世界,很容易就會出現循環引用,好比標準庫Collections中OrderedDict的實現(已去掉無關注釋):

 1 class OrderedDict(dict):
 2     def __init__(self, *args, **kwds):
 3         if len(args) > 1:
 4             raise TypeError('expected at most 1 arguments, got %d' % len(args))
 5         try:
 6             self.__root
 7         except AttributeError:
 8             self.__root = root = []                     # sentinel node
 9             root[:] = [root, root, None]
10             self.__map = {}
11         self.__update(*args, **kwds)

  注意第八、9行,root是一個列表,列表裏面的元素之本身自己!

垃圾回收

  這裏強調一下,本文中的的垃圾回收是狹義的垃圾回收,是指當出現循環引用,引用計數機關用盡的時候採起的垃圾清理算法。

  在python中,使用標記-清除算法(mark-sweep)和分代(generational)算法來垃圾回收。在《Garbage Collection for Python》一文中有對標記回收算法,而後在《Python內存管理機制及優化簡析》一文中,有對前文的翻譯,而且有分代回收的介紹。在這裏,引用後面一篇文章:

  在Python中, 全部可以引用其餘對象的對象都被稱爲容器(container). 所以只有容器之間纔可能造成循環引用. Python的垃圾回收機制利用了這個特色來尋找須要被釋放的對象. 爲了記錄下全部的容器對象, Python將每個 容器都鏈到了一個雙向鏈表中, 之因此使用雙向鏈表是爲了方便快速的在容器集合中插入和刪除對象. 有了這個 維護了全部容器對象的雙向鏈表之後, Python在垃圾回收時使用以下步驟來尋找須要釋放的對象:

  1. 對於每個容器對象, 設置一個gc_refs值, 並將其初始化爲該對象的引用計數值.
  2. 對於每個容器對象, 找到全部其引用的對象, 將被引用對象的gc_refs值減1.
  3. 執行完步驟2之後全部gc_refs值還大於0的對象都被非容器對象引用着, 至少存在一個非循環引用. 所以 不能釋放這些對象, 將他們放入另外一個集合.
  4. 在步驟3中不能被釋放的對象, 若是他們引用着某個對象, 被引用的對象也是不能被釋放的, 所以將這些 對象也放入另外一個集合中.
  5. 此時還剩下的對象都是沒法到達的對象. 如今能夠釋放這些對象了.

   關於分代回收:

  除此以外, Python還將全部對象根據’生存時間’分爲3代, 從0到2. 全部新建立的對象都分配爲第0代. 當這些對象 通過一次垃圾回收仍然存在則會被放入第1代中. 若是第1代中的對象在一次垃圾回收以後仍然存貨則被放入第2代. 對於不一樣代的對象Python的回收的頻率也不同. 能夠經過gc.set_threshold(threshold0[, threshold1[, threshold2]]) 來定義. 當Python的垃圾回收器中新增的對象數量減去刪除的對象數量大於threshold0時, Python會對第0代對象 執行一次垃圾回收. 每當第0代被檢查的次數超過了threshold1時, 第1代對象就會被執行一次垃圾回收. 同理每當 第1代被檢查的次數超過了threshold2時, 第2代對象也會被執行一次垃圾回收.

  注意,threshold0,threshold1,threshold2的意義並不相同

  爲何要分代呢,這個算法的根源來自於weak generational hypothesis。這個假說由兩個觀點構成:首先是年親的對象一般死得也快,好比大量的對象都存在於local做用域;而老對象則頗有可能存活更長的時間,好比全局對象,module, class。

  垃圾回收的原理就如上面提示,詳細的能夠看Python源碼,只不過事實上垃圾回收器還要考慮__del__,弱引用等狀況,會略微複雜一些。

  何時會觸發垃圾回收呢,有三種狀況:

  (1)達到了垃圾回收的閾值,Python虛擬機自動執行

  (2)手動調用gc.collect()

  (3)Python虛擬機退出的時候

 

  對於垃圾回收,有兩個很是重要的術語,那就是reachable與collectable(固然還有與之對應的unreachable與uncollectable),後文也會大量說起。

  reachable是針對python對象而言,若是從根集(root)能到找到對象,那麼這個對象就是reachable,與之相反就是unreachable,事實上就是隻存在於循環引用中的對象,Python的垃圾回收就是針對unreachable對象。

  而collectable是針對unreachable對象而言,若是這種對象能被回收,那麼是collectable;若是不能被回收,即循環引用中的對象定義了__del__, 那麼就是uncollectable。Python垃圾回收對uncollectable對象無能爲力,會形成事實上的內存泄露。

  

gc module

  這裏的gc(garbage collector)是Python 標準庫,該module提供了與上一節「垃圾回收」內容相對應的接口。經過這個module,能夠開關gc、調整垃圾回收的頻率、輸出調試信息。gc模塊是不少其餘模塊(好比objgraph)封裝的基礎,在這裏先介紹gc的核心API。

  gc.enable(); gc.disable(); gc.isenabled()

  開啓gc(默認狀況下是開啓的);關閉gc;判斷gc是否開啓

  gc.collection()

  執行一次垃圾回收,無論gc是否處於開啓狀態都能使用

  gc.set_threshold(t0, t1, t2); gc.get_threshold()

  設置垃圾回收閾值; 得到當前的垃圾回收閾值

  注意:gc.set_threshold(0)也有禁用gc的效果

  gc.get_objects()

  返回全部被垃圾回收器(collector)管理的對象。這個函數很是基礎!只要python解釋器運行起來,就有大量的對象被collector管理,所以,該函數的調用比較耗時!

  好比,命令行啓動python

>>> import gc
>>> len(gc.get_objects())
3749

  gc.get_referents(*obj)

  返回obj對象直接指向的對象

  gc.get_referrers(*obj)

  返回全部直接指向obj的對象

  下面的實例展現了get_referents與get_referrers兩個函數

>>> class OBJ(object):

... pass
...
>>> a, b = OBJ(), OBJ()
>>> hex(id(a)), hex(id(b))
('0x250e730', '0x250e7f0')


>>> gc.get_referents(a)
[<class '__main__.OBJ'>]
>>> a.attr = b
>>> gc.get_referents(a)
[{'attr': <__main__.OBJ object at 0x0250E7F0>}, <class '__main__.OBJ'>]
>>> gc.get_referrers(b)
[{'attr': <__main__.OBJ object at 0x0250E7F0>}, {'a': <__main__.OBJ object at 0x0250E730>, 'b': <__main__.OBJ object at 0x0250E7F0>, 'OBJ': <class '__main__.OBJ'>, '__builtins__': <modu
le '__builtin__' (built-in)>, '__package__': None, 'gc': <module 'gc' (built-in)>, '__name__': '__main__', '__doc__': None}]
>>>

  a, b都是類OBJ的實例,執行"a.attr = b"以後,a就經過‘’attr「這個屬性指向了b。

  gc.set_debug(flags)

  設置調試選項,很是有用,經常使用的flag組合包含如下

  gc.DEBUG_COLLETABLE: 打印能夠被垃圾回收器回收的對象

  gc.DEBUG_UNCOLLETABLE: 打印沒法被垃圾回收器回收的對象,即定義了__del__的對象

  gc.DEBUG_SAVEALL:當設置了這個選項,能夠被拉起回收的對象不會被真正銷燬(free),而是放到gc.garbage這個列表裏面,利於在線上查找問題

內存泄露

  既然Python中經過引用計數和垃圾回收來管理內存,那麼什麼狀況下還會產生內存泄露呢?有兩種狀況:

  第一是對象被另外一個生命週期特別長的對象所引用,好比網絡服務器,可能存在一個全局的單例ConnectionManager,管理全部的鏈接Connection,若是當Connection理論上再也不被使用的時候,沒有從ConnectionManager中刪除,那麼就形成了內存泄露。

  第二是循環引用中的對象定義了__del__函數,這個在《程序員必知的Python陷阱與缺陷列表》一文中有詳細介紹,簡而言之,若是定義了__del__函數,那麼在循環引用中Python解釋器沒法判斷析構對象的順序,所以就不錯處理。

 

  在任何環境,無論是服務器,客戶端,內存泄露都是很是嚴重的事情。

  若是是線上服務器,那麼必定得有監控,若是發現內存使用率超過設置的閾值則當即報警,儘早發現些許還有救。固然,誰也不但願在線上修復內存泄露,這無疑是給行駛的汽車換輪子,所以儘可能在開發環境或者壓力測試環境發現並解決潛在的內存泄露。在這裏,發現問題最爲關鍵,只要發現了問題,解決問題就很是容易了,由於按照前面的說法,出現內存泄露只有兩種狀況,在第一種狀況下,只要在適當的時機解除引用就能夠了;在第二種狀況下,要麼再也不使用__del__函數,換一種實現方式,要麼解決循環引用。

  那麼怎麼查找哪裏存在內存泄露呢?武器就是兩個庫:gc、objgraph

  在上面已經介紹了gc這個模塊,理論上,經過gc模塊可以拿到全部的被garbage collector管理的對象,也能知道對象之間的引用和被引用關係,就能夠畫出對象之間完整的引用關係圖。但事實上仍是比較複雜的,由於在這個過程當中一不當心又會引入新的引用關係,因此,有好的輪子就直接用吧,那就是objgraph

objgraph

  objgraph的實現調用了gc的這幾個函數:gc.get_objects(), gc.get_referents(), gc.get_referers(),而後構造出對象之間的引用關係。objgraph的代碼和文檔都寫得比較好,建議一讀。

  下面先介紹幾個十分實用的API

  def count(typename)

  返回該類型對象的數目,其實就是經過gc.get_objects()拿到所用的對象,而後統計指定類型的數目。

  def by_type(typename)

  返回該類型的對象列表。線上項目,能夠用這個函數很方便找到一個單例對象

  def show_most_common_types(limits = 10)

  打印實例最多的前N(limits)個對象,這個函數很是有用。在《Python內存優化》一文中也提到,該函數能發現能夠用slots進行內存優化的對象

  def show_growth()

  統計自上次調用以來增長得最多的對象,這個函數很是有利於發現潛在的內存泄露。函數內部調用了gc.collect(),所以即便有循環引用也不會對判斷形成影響。

  值得一提,該函數的實現很是有意思,簡化後的代碼以下:

 1 def show_growth(limit=10, peak_stats={}, shortnames=True, file=None):
 2     gc.collect()
 3     stats = typestats(shortnames=shortnames)
 4     deltas = {}
 5     for name, count in iteritems(stats):
 6         old_count = peak_stats.get(name, 0)
 7         if count > old_count:
 8             deltas[name] = count - old_count
 9             peak_stats[name] = count
10     deltas = sorted(deltas.items(), key=operator.itemgetter(1),
11                     reverse=True)

 

  注意形參peak_stats使用了可變參數做爲默認形參,這樣很方便記錄上一次的運行結果。在《程序員必知的Python陷阱與缺陷列表》中提到,使用可變對象作默認形參是最爲常見的python陷阱,但在這裏,卻成爲了方便的利器!

  def show_backrefs()

  生產一張有關objs的引用圖,看出看出對象爲何不釋放,後面會利用這個API來查內存泄露。

  該API有不少有用的參數,好比層數限制(max_depth)、寬度限制(too_many)、輸出格式控制(filename output)、節點過濾(filter, extra_ignore),建議使用之間看一些document。

  def find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()):

  找到一條指向obj對象的最短路徑,且路徑的頭部節點須要知足predicate函數 (返回值爲True)

  能夠快捷、清晰指出 對象的被引用的狀況,後面會展現這個函數的威力

  def show_chain():

  將find_backref_chain 找到的路徑畫出來, 該函數事實上調用show_backrefs,只是排除了全部不在路徑中的節點。

查找內存泄露

  在這一節,介紹如何利用objgraph來查找內存是怎麼泄露的

  若是咱們懷疑一段代碼、一個模塊可能會致使內存泄露,那麼首先調用一次obj.show_growth(),而後調用相應的函數,最後再次調用obj.show_growth(),看看是否有增長的對象。好比下面這個簡單的例子:

 1 # -*- coding: utf-8 -*-
 2 import objgraph
 3 
 4 _cache = []
 5 
 6 class OBJ(object):
 7     pass
 8 
 9 def func_to_leak():
10     o  = OBJ()
11     _cache.append(o)
12     # do something with o, then remove it from _cache 
13 
14     if True: # this seem ugly, but it always exists
15         return 
16     _cache.remove(o)
17 
18 if __name__ == '__main__':
19     objgraph.show_growth()
20     try:
21         func_to_leak()
22     except:
23         pass
24     print 'after call func_to_leak'
25     objgraph.show_growth()

  運行結果(咱們只關心後一次show_growth的結果)以下

wrapper_descriptor 1073 +13
member_descriptor 204 +5
getset_descriptor 168 +5
weakref 338 +3
dict 458 +3
OBJ 1 +1

  代碼很簡單,函數開始的時候講對象加入了global做用域的_cache列表,而後指望是在函數退出以前從_cache刪除,可是因爲提早返回或者異常,並無執行到最後的remove語句。從運行結果能夠發現,調用函數以後,增長了一個類OBJ的實例,然而理論上函數調用結束以後,全部在函數做用域(local)中聲明的對象都改被銷燬,所以這裏就存在內存泄露。

  固然,在實際的項目中,咱們也不清楚泄露是在哪段代碼、哪一個模塊中發生的,並且每每是發生了內存泄露以後再去排查,這個時候使用obj.show_most_common_types就比較合適了,若是一個自定義的類的實例數目特別多,那麼就可能存在內存泄露。若是在壓力測試環境,中止壓測,調用gc.collet,而後再用obj.show_most_common_types查看,若是對象的數目沒有相應的減小,那麼確定就是存在泄露。

  當咱們定位了哪一個對象發生了內存泄露,那麼接下來就是分析怎麼泄露的,引用鏈是怎麼樣的,這個時候就該show_backrefs出馬了,仍是以以前的代碼爲例,稍加修改:

 1 import objgraph
 2 
 3 _cache = []
 4 
 5 class OBJ(object):
 6     pass
 7 
 8 def func_to_leak():
 9     o  = OBJ()
10     _cache.append(o)
11     # do something with o, then remove it from _cache 
12 
13     if True: # this seem ugly, but it always exists
14         return 
15     _cache.remove(o)
16 
17 if __name__ == '__main__':
18     try:
19         func_to_leak()
20     except:
21         pass
22     objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth = 10, filename = 'obj.dot')
show_backrefs查看內存泄露

  注意,上面的代碼中,max_depth參數很是關鍵,若是這個參數過小,那麼看不到完整的引用鏈,若是這個參數太大,運行的時候又很是耗時間。

  而後打開dot文件,結果以下

  

  能夠看到泄露的對象(紅框表示),是被一個叫_cache的list所引用,而_cache又是被__main__這個module所引用。

  對於示例代碼,dot文件的結果已經很是清晰,可是對於真實項目,引用鏈中的節點可能成百上千,看起來很是頭大,下面用tornado起一個最最簡單的web服務器(代碼不知道來自哪裏,且沒有內存泄露,這裏只是爲了顯示引用關係),而後繪製socket的引用關關係圖,代碼和引用關係圖以下:

 1 import objgraph
 2 import errno
 3 import functools
 4 import tornado.ioloop
 5 import socket
 6 
 7 def connection_ready(sock, fd, events):
 8     while True:
 9         try:
10             connection, address = sock.accept()
11             print 'connection_ready', address
12         except socket.error as e:
13             if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
14                 raise
15             return
16         connection.setblocking(0)
17         # do sth with connection
18 
19 
20 if __name__ == '__main__':
21     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
22     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
23     sock.setblocking(0)
24     sock.bind(("", 8888))
25     sock.listen(128)
26 
27     io_loop = tornado.ioloop.IOLoop.current()
28     callback = functools.partial(connection_ready, sock)
29     io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
30     #objgraph.show_backrefs(sock, max_depth = 10, filename = 'tornado.dot')
31     # objgraph.show_chain(
32     #     objgraph.find_backref_chain(
33     #         sock,
34     #         objgraph.is_proper_module
35     #     ),
36     #     filename='obj_chain.dot'
37     # )
38     io_loop.start()
tornado_server實例

  

  可見,代碼越複雜,相互之間的引用關係越多,show_backrefs越難以看懂。這個時候就使用show_chain和find_backref_chain吧,這種方法,在官方文檔也是推薦的,咱們稍微改改代碼,結果以下:

 1 import objgraph
 2 
 3 _cache = []
 4 
 5 class OBJ(object):
 6     pass
 7 
 8 def func_to_leak():
 9     o  = OBJ()
10     _cache.append(o)
11     # do something with o, then remove it from _cache 
12 
13     if True: # this seem ugly, but it always exists
14         return 
15     _cache.remove(o)
16 
17 if __name__ == '__main__':
18     try:
19         func_to_leak()
20     except:
21         pass
22     # objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth = 10, filename = 'obj.dot')
23     objgraph.show_chain(
24         objgraph.find_backref_chain(
25             objgraph.by_type('OBJ')[0],
26             objgraph.is_proper_module
27         ),
28         filename='obj_chain.dot'
29     )
show_chain查看內存泄露

  

  

  上面介紹了內存泄露的第一種狀況,對象被「非指望」地引用着。下面看看第二種狀況,循環引用中的__del__, 看下面的代碼:

 1 # -*- coding: utf-8 -*-
 2 import objgraph, gc
 3 class OBJ(object):
 4     def __del__(self):
 5         print('Dangerous!')
 6 
 7 def show_leak_by_del():
 8     a, b = OBJ(), OBJ()
 9     a.attr_b = b
10     b.attr_a = a
11 
12     del a, b
13     print gc.collect()
14 
15     objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth = 10, filename = 'del_obj.dot')

  上面的代碼存在循環引用,並且OBJ類定義了__del__函數。若是沒有定義__del__函數,那麼上述的代碼會報錯, 由於gc.collect會將循環引用刪除,objgraph.by_type('OBJ')返回空列表。而由於定義了__del__函數,gc.collect也無能爲力,結果以下:

  

  從圖中能夠看到,對於這種狀況,仍是比較好辨識的,由於objgraph將__del__函數用特殊顏色標誌出來,一眼就看見了。另外,能夠看見gc.garbage(類型是list)也引用了這兩個對象,緣由在document中有描述,當執行垃圾回收的時候,會將定義了__del__函數的類實例(被稱爲uncollectable object)放到gc.garbage列表,所以,也能夠直接經過查看gc.garbage來找出定義了__del__的循環引用。在這裏,經過增長extra_ignore來排除gc.garbage的影響:

   將上述代碼的最後一行改爲:

  objgraph.show_backrefs(objgraph.by_type('OBJ')[0], extra_ignore=(id(gc.garbage),),  max_depth = 10, filename = 'del_obj.dot')

   

  

   另外,也能夠設置DEBUG_UNCOLLECTABLE 選項,直接將uncollectable對象輸出到標準輸出,而不是放到gc.garbage

循環引用

  除非定義了__del__方法,那麼循環引用也不是什麼萬惡不赦的東西,由於垃圾回收器能夠處理循環引用,並且不許是python標準庫仍是大量使用的第三方庫,均可能存在循環引用。若是存在循環引用,那麼Python的gc就必須開啓(gc.isenabled()返回True),不然就會內存泄露。可是在某些狀況下,咱們仍是不但願有gc,好比對內存和性能比較敏感的應用場景,在這篇文章中,提到instagram經過禁用gc,性能提高了10%;另外,在一些應用場景,垃圾回收帶來的卡頓也是不能接受的,好比RPG遊戲。從前面對垃圾回收的描述能夠看到,執行一次垃圾回收是很耗費時間的,由於須要遍歷全部被collector管理的對象(即便不少對象不屬於垃圾)。所以,要想禁用GC,就得先完全乾掉循環引用。

  同內存泄露同樣,解除循環引用的前提是定位哪裏出現了循環引用。並且,若是須要在線上應用關閉gc,那麼須要自動、持久化的進行檢測。下面介紹如何定位循環引用,以及如何解決循環引用。

定位循環引用

  這裏仍是是用GC模塊和objgraph來定位循環引用。須要注意的事,必定要先禁用gc(調用gc.disable()), 防止偏差。

  這裏利用以前介紹循環引用時使用過的例子: a, b兩個OBJ對象造成循環引用

 1 # -*- coding: utf-8 -*-
 2 import objgraph, gc
 3 class OBJ(object):
 4     pass
 5 
 6 def show_cycle_reference():
 7     a, b = OBJ(), OBJ()
 8     a.attr_b = b
 9     b.attr_a = a
10 
11 if __name__ == '__main__':
12     gc.disable()
13     for _ in xrange(50):
14         show_cycle_reference()
15     objgraph.show_most_common_types(20)

  運行結果(部分):

wrapper_descriptor 1060
dict 555
OBJ 100

  上面的代碼中使用的是show_most_common_types,而沒有使用show_growth(由於growth會手動調用gc.collect()),經過結果能夠看到,內存中如今有100個OBJ對象,符合預期。固然這些OBJ對象沒有在函數調用後被銷燬,不必定是循環引用的問題,也多是內存泄露,好比前面OBJ對象被global做用域中的_cache引用的狀況。怎麼排除是不是被global做用域的變量引用的狀況呢,方法仍是objgraph.find_backref_chain(obj),在__doc__中指出,若是找不到符合條件的應用鏈(chain),那麼返回[obj],稍微修改上面的代碼:

 1 # -*- coding: utf-8 -*-
 2 import objgraph, gc
 3 class OBJ(object):
 4     pass
 5 
 6 def show_cycle_reference():
 7     a, b = OBJ(), OBJ()
 8     a.attr_b = b
 9     b.attr_a = a
10 
11 if __name__ == '__main__':
12     gc.disable()
13     for _ in xrange(50):
14         show_cycle_reference()
15     ret = objgraph.find_backref_chain(objgraph.by_type('OBJ')[0], objgraph.is_proper_module)
16     print ret
純循環引用判斷

  上面的代碼輸出:

[<__main__.OBJ object at 0x0244F810>]  

  驗證了咱們的想法,OBJ對象不是被global做用域的變量所引用。

  

  在實際項目中,不大可能處處用objgraph.show_most_common_types或者objgraph.by_type來排查循環引用,效率過低。有沒有更好的辦法呢,有的,那就是使用gc模塊的debug 選項。在前面介紹gc模塊的時候,就介紹了gc.DEBUG_COLLECTABLE 選項,咱們來試試:

 1 # -*- coding: utf-8 -*-
 2 import gc, time
 3 class OBJ(object):
 4     pass
 5 
 6 def show_cycle_reference():
 7     a, b = OBJ(), OBJ()
 8     a.attr_b = b
 9     b.attr_a = a
10 
11 if __name__ == '__main__':
12     gc.disable() # 這裏是否disable事實上無所謂
13     gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_OBJECTS)
14     for _ in xrange(1):
15         show_cycle_reference()
16     gc.collect()
17     time.sleep(5)

  上面代碼第13行設置了debug flag,能夠打印出collectable對象。另外,只用調用一次show_cycle_reference函數就足夠了(這也比objgraph.show_most_common_types方便一點)。在第16行手動調用gc.collect(),輸出以下:

gc: collectable <OBJ 023B46F0>
gc: collectable <OBJ 023B4710>
gc: collectable <dict 023B7AE0>
gc: collectable <dict 023B7930>

  注意:只有當對象是unreachable且collectable的時候,在collect的時候纔會被輸出,也就是說,若是是reachable,好比被global做用域的變量引用,那麼也是不會輸出的。

  經過上面的輸出,咱們已經知道OBJ類的實例存在循環引用,可是這個時候,obj實例已經被回收了。那麼若是我想經過show_backrefs找出這個引用關係,須要從新調用show_cycle_reference函數,而後不調用gc.collect,經過show_backrefs 和 by_type繪製。有沒有更好的辦法呢,可讓我在一次運行中發現循環引用,並找出引用鏈?答案就是使用DEBUG_SAVEALL,下面爲了展現方便,直接在命令行中操做(固然,使用ipython更好)

>>> import gc, objgraph
>>> class OBJ(object):
... pass
...
>>> def show_cycle_reference():
... a, b = OBJ(), OBJ()
... a.attr_b = b
... b.attr_a = a
...
>>> gc.set_debug(gc.DEBUG_SAVEALL| gc.DEBUG_OBJECTS)
>>> show_cycle_reference()
>>> print 'before collect', gc.garbage
before collect []
>>> print gc.collect()
4
>>>
>>> for o in gc.garbage:
... print o
...
<__main__.OBJ object at 0x024BB7D0>
<__main__.OBJ object at 0x02586850>
{'attr_b': <__main__.OBJ object at 0x02586850>}
{'attr_a': <__main__.OBJ object at 0x024BB7D0>}
>>>
>>> objgraph.show_backrefs(objgraph.at(0x024BB7D0), 5, filename = 'obj.dot')
Graph written to obj.dot (13 nodes)
>>>

  上面在調用gc.collect以前,gc.garbage裏面是空的,因爲設置了DEBUG_SAVEALL,那麼調用gc.collect時,會將collectable對象放到gc.garbage。此時,對象沒有被釋放,咱們就能夠直接繪製出引用關係,這裏使用了objgraph.at,固然也可使用objgraph.by_type, 或者直接從gc.garbage取對象,結果以下:

  

  出了循環引用,能夠看見還有兩個引用,gc.garbage與局部變量o,相信你們也能理解。

  

 

 

消滅循環引用

  找到循環引用關係以後,解除循環引用就不是太難的事情,總的來講,有兩種辦法:手動解除與使用weakref。

  手動解除很好理解,就是在合適的時機,解除引用關係。好比,前面提到的collections.OrderedDict:

>>> root = []
>>> root[:] = [root, root, None]
>>>
>>> root
[[...], [...], None]
>>>
>>> del root[:]
>>> root
[]

  更常見的狀況,是咱們自定義的對象之間存在循環引用:要麼是單個對象內的循環引用,要麼是多個對象間的循環引用,咱們看一個單個對象內循環引用的例子:

 1 class Connection(object):
 2     MSG_TYPE_CHAT = 0X01
 3     MSG_TYPE_CONTROL = 0X02
 4     def __init__(self):
 5         self.msg_handlers = {
 6             self.MSG_TYPE_CHAT : self.handle_chat_msg,
 7             self.MSG_TYPE_CONTROL : self.handle_control_msg
 8         } 
 9 
10     def on_msg(self, msg_type, *args):
11         self.msg_handlers[msg_type](*args)
12 
13     def handle_chat_msg(self, msg):
14         pass
15 
16     def handle_control_msg(self, msg):
17         pass

 

  上面的代碼很是常見,代碼也很簡單,初始化函數中爲每種消息類型定義響應的處理函數,當消息到達(on_msg)時根據消息類型取出處理函數。但這樣的代碼是存在循環引用的,感興趣的讀者能夠用objgraph看看引用圖。如何手動解決呢,爲Connection增長一個destroy(或者叫clear)函數,該函數將 self.msg_handlers 清空(self.msg_handlers.clear())。當Connection理論上不在被使用的時候調用destroy函數便可。

  對於多個對象間的循環引用,處理方法也是同樣的,就是在「適當的時機」調用destroy函數,難點在於什麼是適當的時機

  

  另一種更方便的方法,就是使用弱引用weakref, weakref是Python提供的標準庫,旨在解決循環引用。

  weakref模塊提供瞭如下一些有用的API:

  (1)weakref.ref(object, callback = None)

  建立一個對object的弱引用,返回值爲weakref對象,callback: 當object被刪除的時候,會調用callback函數,在標準庫logging (__init__.py)中有使用範例。使用的時候要用()解引用,若是referant已經被刪除,那麼返回None。好比下面的例子

 1 # -*- coding: utf-8 -*-
 2 import weakref
 3 class OBJ(object):
 4     def f(self):
 5         print 'HELLO'
 6 
 7 if __name__ == '__main__':
 8     o = OBJ()
 9     w = weakref.ref(o)
10     w().f()
11     del o
12     w().f()

 

  運行上面的代碼,第12行會拋出異常:AttributeError: 'NoneType' object has no attribute 'f'。由於這個時候被引用的對象已經被刪除了

  (2)weakref.proxy(object, callback = None)

  建立一個代理,返回值是一個weakproxy對象,callback的做用同上。使用的時候直接用 和object同樣,若是object已經被刪除 那麼跑出異常   ReferenceError: weakly-referenced object no longer exists。

 1 # -*- coding: utf-8 -*-
 2 import weakref
 3 class OBJ(object):
 4     def f(self):
 5         print 'HELLO'
 6 
 7 if __name__ == '__main__':
 8     o = OBJ()
 9     w = weakref.proxy(o)
10     w.f()
11     del o
12     w.f()

  注意第10行 12行與weakref.ref示例代碼的區別

  (3)weakref.WeakSet

  這個是一個弱引用集合,當WeakSet中的元素被回收的時候,會自動從WeakSet中刪除。WeakSet的實現使用了weakref.ref,當對象加入WeakSet的時候,使用weakref.ref封裝,指定的callback函數就是從WeakSet中刪除。感興趣的話能夠直接看源碼(_weakrefset.py),下面給出一個參考例子:

 1 # -*- coding: utf-8 -*-
 2 import weakref
 3 class OBJ(object):
 4     def f(self):
 5         print 'HELLO'
 6 
 7 if __name__ == '__main__':
 8     o = OBJ()
 9     ws = weakref.WeakSet()
10     ws.add(o)
11     print len(ws) #  1
12     del o
13     print len(ws) # 0

  (4)weakref.WeakValueDictionary, weakref.WeakKeyDictionary

  實現原理和使用方法基本同WeakSet

 

總結

   本文的篇幅略長,首選是簡單介紹了python的內存管理,重點介紹了引用計數與垃圾回收,而後闡述Python中內存泄露與循環引用產生的緣由與危害,最後是利用gc、objgraph、weakref等工具來分析並解決內存泄露、循環引用問題。

 

references

Garbage Collector Interface

objgraph

Garbage Collection for Python

禁用Python的GC機制後,Instagram性能提高10%

Python內存管理機制及優化簡析

library weakref

相關文章
相關標籤/搜索