日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
使用GC、Objgraph干掉Python內(nèi)存泄露與循環(huán)引用!

Python使用引用計(jì)數(shù)和垃圾回收來(lái)做內(nèi)存管理,前面也寫(xiě)過(guò)一遍文章《Python內(nèi)存優(yōu)化》,介紹了在python中,如何profile內(nèi)存使用情況,并做出相應(yīng)的優(yōu)化。本文介紹兩個(gè)更致命的問(wèn)題:內(nèi)存泄露與循環(huán)引用。內(nèi)存泄露是讓所有程序員都聞風(fēng)喪膽的問(wèn)題,輕則導(dǎo)致程序運(yùn)行速度減慢,重則導(dǎo)致程序崩潰;而循環(huán)引用是使用了引用計(jì)數(shù)的數(shù)據(jù)結(jié)構(gòu)、編程語(yǔ)言都需要解決的問(wèn)題。本文揭曉這兩個(gè)問(wèn)題在python語(yǔ)言中是如何存在的,然后試圖利用gc模塊和objgraph來(lái)解決這兩個(gè)問(wèn)題。

成都一家集口碑和實(shí)力的網(wǎng)站建設(shè)服務(wù)商,擁有專(zhuān)業(yè)的企業(yè)建站團(tuán)隊(duì)和靠譜的建站技術(shù),十多年企業(yè)及個(gè)人網(wǎng)站建設(shè)經(jīng)驗(yàn) ,為成都近1000家客戶(hù)提供網(wǎng)頁(yè)設(shè)計(jì)制作,網(wǎng)站開(kāi)發(fā),企業(yè)網(wǎng)站制作建設(shè)等服務(wù),包括成都營(yíng)銷(xiāo)型網(wǎng)站建設(shè),高端網(wǎng)站設(shè)計(jì),同時(shí)也為不同行業(yè)的客戶(hù)提供網(wǎng)站制作、網(wǎng)站建設(shè)的服務(wù),包括成都電商型網(wǎng)站制作建設(shè),裝修行業(yè)網(wǎng)站制作建設(shè),傳統(tǒng)機(jī)械行業(yè)網(wǎng)站建設(shè),傳統(tǒng)農(nóng)業(yè)行業(yè)網(wǎng)站制作建設(shè)。在成都做網(wǎng)站,選網(wǎng)站制作建設(shè)服務(wù)商就選創(chuàng)新互聯(lián)公司

注意:本文的目標(biāo)是Cpython,測(cè)試代碼都是運(yùn)行在Python2.7。另外,本文不考慮C擴(kuò)展造成的內(nèi)存泄露,這是另一個(gè)復(fù)雜且頭疼的問(wèn)題。

一分鐘版本

  1. python使用引用計(jì)數(shù)和垃圾回收來(lái)釋放(free)Python對(duì)象
  2. 引用計(jì)數(shù)的優(yōu)點(diǎn)是原理簡(jiǎn)單、將消耗均攤到運(yùn)行時(shí);缺點(diǎn)是無(wú)法處理循環(huán)引用
  3. Python垃圾回收用于處理循環(huán)引用,但是無(wú)法處理循環(huán)引用中的對(duì)象定義了__del__的情況,而且每次回收會(huì)造成一定的卡頓
  4. gc module是python垃圾回收機(jī)制的接口模塊,可以通過(guò)該module啟停垃圾回收、調(diào)整回收觸發(fā)的閾值、設(shè)置調(diào)試選項(xiàng)
  5. 如果沒(méi)有禁用垃圾回收,那么Python中的內(nèi)存泄露有兩種情況:要么是對(duì)象被生命周期更長(zhǎng)的對(duì)象所引用,比如global作用域?qū)ο?要么是循環(huán)引用中存在__del__
  6. 使用gc module、objgraph可以定位內(nèi)存泄露,定位之后,解決很簡(jiǎn)單
  7. 垃圾回收比較耗時(shí),因此在對(duì)性能和內(nèi)存比較敏感的場(chǎng)景也是無(wú)法接受的,如果能解除循環(huán)引用,就可以禁用垃圾回收。
  8. 使用gc module的DEBUG選項(xiàng)可以很方便的定位循環(huán)引用,解除循環(huán)引用的辦法要么是手動(dòng)解除,要么是使用weakref

python內(nèi)存管理

Python中,一切都是對(duì)象,又分為mutable和immutable對(duì)象。二者區(qū)分的標(biāo)準(zhǔn)在于是否可以原地修改,“原地“”可以理解為相同的地址。可以通過(guò)id()查看一個(gè)對(duì)象的“地址”,如果通過(guò)變量修改對(duì)象的值,但id沒(méi)發(fā)生變化,那么就是mutable,否則就是immutable。比如:

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

a指向的對(duì)象(int類(lèi)型)就是immutable, 賦值語(yǔ)句只是讓變量a指向了一個(gè)新的對(duì)象,因?yàn)閕d發(fā)生了變化。而lst指向的對(duì)象(list類(lèi)型)為可變對(duì)象,通過(guò)方法(append)可以修改對(duì)象的值,同時(shí)保證id一致。

判斷兩個(gè)變量是否相等(值相同)使用==, 而判斷兩個(gè)變量是否指向同一個(gè)對(duì)象使用 is。比如下面a1 a2這兩個(gè)變量指向的都是空的列表,值相同,但是不是同一個(gè)對(duì)象。

 
 
 
 
  1. >>> a1, a2 = [], [] 
  2. >>> a1 == a2 
  3. True 
  4. >>> a1 is a2 
  5. False  

為了避免頻繁的申請(qǐng)、釋放內(nèi)存,避免大量使用的小對(duì)象的構(gòu)造析構(gòu),python有一套自己的內(nèi)存管理機(jī)制。在巨著《Python源碼剖析》中有詳細(xì)介紹,在python源碼obmalloc.h中也有詳細(xì)的描述。如下所示: 

可以看到,python會(huì)有自己的內(nèi)存緩沖池(layer2)以及對(duì)象緩沖池(layer3)。在Linux上運(yùn)行過(guò)Python服務(wù)器的程序都知道,python不會(huì)立即將釋放的內(nèi)存歸還給操作系統(tǒng),這就是內(nèi)存緩沖池的原因。而對(duì)于可能被經(jīng)常使用、而且是immutable的對(duì)象,比如較小的整數(shù)、長(zhǎng)度較短的字符串,python會(huì)緩存在layer3,避免頻繁創(chuàng)建和銷(xiāo)毀。例如:

 
 
 
 
  1. >>> a, b = 1, 1 
  2. >>> a is b 
  3. True 
  4. >>> a, b = (), () 
  5. >>> a is b 
  6. True 
  7. >>> a, b = {}, {} 
  8. >>> a is b 
  9. False  

本文并不關(guān)心python是如何管理內(nèi)存塊、如何管理小對(duì)象,感興趣的讀者可以參考伯樂(lè)在線(xiàn)和csdn上的這兩篇文章。

本文關(guān)心的是,一個(gè)普通的對(duì)象的生命周期,更明確的說(shuō),對(duì)象是什么時(shí)候被釋放的。當(dāng)一個(gè)對(duì)象理論上(或者邏輯上)不再被使用了,但事實(shí)上沒(méi)有被釋放,那么就存在內(nèi)存泄露;當(dāng)一個(gè)對(duì)象事實(shí)上已經(jīng)不可達(dá)(unreachable),即不能通過(guò)任何變量找到這個(gè)對(duì)象,但這個(gè)對(duì)象沒(méi)有立即被釋放,那么則可能存在循環(huán)引用。

引用計(jì)數(shù)

引用計(jì)數(shù)(References count),指的是每個(gè)Python對(duì)象都有一個(gè)計(jì)數(shù)器,記錄著當(dāng)前有多少個(gè)變量指向這個(gè)對(duì)象。

將一個(gè)對(duì)象直接或者間接賦值給一個(gè)變量時(shí),對(duì)象的計(jì)數(shù)器會(huì)加1;當(dāng)變量被del刪除,或者離開(kāi)變量所在作用域時(shí),對(duì)象的引用計(jì)數(shù)器會(huì)減1。當(dāng)計(jì)數(shù)器歸零的時(shí)候,代表這個(gè)對(duì)象再也沒(méi)有地方可能使用了,因此可以將對(duì)象安全的銷(xiāo)毀。Python源碼中,通過(guò)Py_INCREF和Py_DECREF兩個(gè)宏來(lái)管理對(duì)象的引用計(jì)數(shù),代碼在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)  

通過(guò)sys.getrefcount(obj)對(duì)象可以獲得一個(gè)對(duì)象的引用數(shù)目,返回值是真實(shí)引用數(shù)目加1(加1的原因是obj被當(dāng)做參數(shù)傳入了getrefcount函數(shù)),例如:

 
 
 
 
  1. >>> import sys 
  2. >>> s = 'asdf' 
  3. >>> sys.getrefcount(s) 
  4. >>> a = 1 
  5. >>> sys.getrefcount(a) 
  6. 605  

從對(duì)象1的引用計(jì)數(shù)信息也可以看到,python的對(duì)象緩沖池會(huì)緩存十分常用的immutable對(duì)象,比如這里的整數(shù)1。

引用計(jì)數(shù)的優(yōu)點(diǎn)在于原理通俗易懂;且將對(duì)象的回收分布在代碼運(yùn)行時(shí):一旦對(duì)象不再被引用,就會(huì)被釋放掉(be freed),不會(huì)造成卡頓。但也有缺點(diǎn):額外的字段(ob_refcnt);頻繁的加減ob_refcnt,而且可能造成連鎖反應(yīng)。但這些缺點(diǎn)跟循環(huán)引用比起來(lái)都不算事兒。

什么是循環(huán)引用,就是一個(gè)對(duì)象直接或者間接引用自己本身,引用鏈形成一個(gè)環(huán)。且看下面的例子:

 
 
 
 
  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()  

運(yùn)行上面的代碼,使用graphviz工具集(本文使用的是dotty)打開(kāi)生成的兩個(gè)文件,direct.dot 和 indirect.dot,得到下面兩個(gè)圖

通過(guò)屬性名(attr, attr_a, attr_b)可以很清晰的看出循環(huán)引用是怎么產(chǎn)生的

前面已經(jīng)提到,對(duì)于一個(gè)對(duì)象,當(dāng)沒(méi)有任何變量指向自己時(shí),引用計(jì)數(shù)降到0,就會(huì)被釋放掉。我們以上面左邊那個(gè)圖為例,可以看到,紅框里面的OBJ對(duì)象想在有兩個(gè)引用(兩個(gè)入度),分別來(lái)自幀對(duì)象frame(代碼中,函數(shù)局部空間持有對(duì)OBJ實(shí)例的引用)、attr變量。我們?cè)俑囊幌麓a,在函數(shù)運(yùn)行技術(shù)之后看看是否還有OBJ類(lèi)的實(shí)例存在,引用關(guān)系是怎么樣的:

 
 
 
 
  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" 

 

修改后的代碼,OBJ實(shí)例(a)存在于函數(shù)的local作用域。因此,當(dāng)函數(shù)調(diào)用結(jié)束之后,來(lái)自幀對(duì)象frame的引用被解除。從圖中可以看到,當(dāng)前對(duì)象的計(jì)數(shù)器(入度)為1,按照引用計(jì)數(shù)的原理,是不應(yīng)該被釋放的,但這個(gè)對(duì)象在函數(shù)調(diào)用結(jié)束之后就是事實(shí)上的垃圾,這個(gè)時(shí)候就需要另外的機(jī)制來(lái)處理這種情況了。

python的世界,很容易就會(huì)出現(xiàn)循環(huán)引用,比如標(biāo)準(zhǔn)庫(kù)Collections中OrderedDict的實(shí)現(xiàn)(已去掉無(wú)關(guān)注釋):

 
 
 
 
  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)  

注意第8、9行,root是一個(gè)列表,列表里面的元素之自己本身!

垃圾回收

這里強(qiáng)調(diào)一下,本文中的的垃圾回收是狹義的垃圾回收,是指當(dāng)出現(xiàn)循環(huán)引用,引用計(jì)數(shù)無(wú)計(jì)可施的時(shí)候采取的垃圾清理算法。

在python中,使用標(biāo)記-清除算法(mark-sweep)和分代(generational)算法來(lái)垃圾回收。在《Garbage Collection for Python》一文中有對(duì)標(biāo)記回收算法,然后在《Python內(nèi)存管理機(jī)制及優(yōu)化簡(jiǎn)析》一文中,有對(duì)前文的翻譯,并且有分代回收的介紹。在這里,引用后面一篇文章:

在Python中, 所有能夠引用其他對(duì)象的對(duì)象都被稱(chēng)為容器(container). 因此只有容器之間才可能形成循環(huán)引用. Python的垃圾回收機(jī)制利用了這個(gè)特點(diǎn)來(lái)尋找需要被釋放的對(duì)象. 為了記錄下所有的容器對(duì)象, Python將每一個(gè) 容器都鏈到了一個(gè)雙向鏈表中, 之所以使用雙向鏈表是為了方便快速的在容器集合中插入和刪除對(duì)象. 有了這個(gè) 維護(hù)了所有容器對(duì)象的雙向鏈表以后, Python在垃圾回收時(shí)使用如下步驟來(lái)尋找需要釋放的對(duì)象:

  1. 對(duì)于每一個(gè)容器對(duì)象, 設(shè)置一個(gè)gc_refs值, 并將其初始化為該對(duì)象的引用計(jì)數(shù)值.
  2. 對(duì)于每一個(gè)容器對(duì)象, 找到所有其引用的對(duì)象, 將被引用對(duì)象的gc_refs值減1.
  3. 執(zhí)行完步驟2以后所有g(shù)c_refs值還大于0的對(duì)象都被非容器對(duì)象引用著, 至少存在一個(gè)非循環(huán)引用. 因此 不能釋放這些對(duì)象, 將他們放入另一個(gè)集合.
  4. 在步驟3中不能被釋放的對(duì)象, 如果他們引用著某個(gè)對(duì)象, 被引用的對(duì)象也是不能被釋放的, 因此將這些 對(duì)象也放入另一個(gè)集合中.
  5. 此時(shí)還剩下的對(duì)象都是無(wú)法到達(dá)的對(duì)象. 現(xiàn)在可以釋放這些對(duì)象了.

關(guān)于分代回收:

除此之外, Python還將所有對(duì)象根據(jù)’生存時(shí)間’分為3代, 從0到2. 所有新創(chuàng)建的對(duì)象都分配為第0代. 當(dāng)這些對(duì)象 經(jīng)過(guò)一次垃圾回收仍然存在則會(huì)被放入第1代中. 如果第1代中的對(duì)象在一次垃圾回收之后仍然存貨則被放入第2代. 對(duì)于不同代的對(duì)象Python的回收的頻率也不一樣. 可以通過(guò)gc.set_threshold(threshold0[, threshold1[, threshold2]]) 來(lái)定義. 當(dāng)Python的垃圾回收器中新增的對(duì)象數(shù)量減去刪除的對(duì)象數(shù)量大于threshold0時(shí), Python會(huì)對(duì)第0代對(duì)象 執(zhí)行一次垃圾回收. 每當(dāng)?shù)?代被檢查的次數(shù)超過(guò)了threshold1時(shí), 第1代對(duì)象就會(huì)被執(zhí)行一次垃圾回收. 同理每當(dāng) 第1代被檢查的次數(shù)超過(guò)了threshold2時(shí), 第2代對(duì)象也會(huì)被執(zhí)行一次垃圾回收.

注意,threshold0,threshold1,threshold2的意義并不相同!

為什么要分代呢,這個(gè)算法的根源來(lái)自于weak generational hypothesis。這個(gè)假說(shuō)由兩個(gè)觀點(diǎn)構(gòu)成:首先是年親的對(duì)象通常死得也快,比如大量的對(duì)象都存在于local作用域;而老對(duì)象則很有可能存活更長(zhǎng)的時(shí)間,比如全局對(duì)象,module, class。

垃圾回收的原理就如上面提示,詳細(xì)的可以看Python源碼,只不過(guò)事實(shí)上垃圾回收器還要考慮__del__,弱引用等情況,會(huì)略微復(fù)雜一些。

什么時(shí)候會(huì)觸發(fā)垃圾回收呢,有三種情況:

  1. 達(dá)到了垃圾回收的閾值,Python虛擬機(jī)自動(dòng)執(zhí)行
  2. 手動(dòng)調(diào)用gc.collect()
  3. Python虛擬機(jī)退出的時(shí)候

對(duì)于垃圾回收,有兩個(gè)非常重要的術(shù)語(yǔ),那就是reachable與collectable(當(dāng)然還有與之對(duì)應(yīng)的unreachable與uncollectable),后文也會(huì)大量提及。

reachable是針對(duì)python對(duì)象而言,如果從根集(root)能到找到對(duì)象,那么這個(gè)對(duì)象就是reachable,與之相反就是unreachable,事實(shí)上就是只存在于循環(huán)引用中的對(duì)象,Python的垃圾回收就是針對(duì)unreachable對(duì)象。

而collectable是針對(duì)unreachable對(duì)象而言,如果這種對(duì)象能被回收,那么是collectable;如果不能被回收,即循環(huán)引用中的對(duì)象定義了__del__, 那么就是uncollectable。Python垃圾回收對(duì)uncollectable對(duì)象無(wú)能為力,會(huì)造成事實(shí)上的內(nèi)存泄露。

gc module

這里的gc(garbage collector)是Python 標(biāo)準(zhǔn)庫(kù),該module提供了與上一節(jié)“垃圾回收”內(nèi)容相對(duì)應(yīng)的接口。通過(guò)這個(gè)module,可以開(kāi)關(guān)gc、調(diào)整垃圾回收的頻率、輸出調(diào)試信息。gc模塊是很多其他模塊(比如objgraph)封裝的基礎(chǔ),在這里先介紹gc的核心API。

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

開(kāi)啟gc(默認(rèn)情況下是開(kāi)啟的);關(guān)閉gc;判斷gc是否開(kāi)啟

 
 
 
 
  1. gc.collection() 

執(zhí)行一次垃圾回收,不管gc是否處于開(kāi)啟狀態(tài)都能使用

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

設(shè)置垃圾回收閾值; 獲得當(dāng)前的垃圾回收閾值

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

 
 
 
 
  1. gc.get_objects() 

返回所有被垃圾回收器(collector)管理的對(duì)象。這個(gè)函數(shù)非?;A(chǔ)!只要python解釋器運(yùn)行起來(lái),就有大量的對(duì)象被collector管理,因此,該函數(shù)的調(diào)用比較耗時(shí)!

比如,命令行啟動(dòng)python

 
 
 
 
  1. >>> import gc 
  2. >>> len(gc.get_objects()) 
  3. 3749  
 
 
 
 
  1. gc.get_referents(*obj) 

返回obj對(duì)象直接指向的對(duì)象

 
 
 
 
  1. gc.get_referrers(*obj) 

返回所有直接指向obj的對(duì)象

下面的實(shí)例展示了get_referents與get_referrers兩個(gè)函數(shù)

 
 
 
 
  1. >>> class OBJ(object): 
  2.   
  3. ... pass 
  4. ... 
  5. >>> a, b = OBJ(), OBJ() 
  6. >>> hex(id(a)), hex(id(b)) 
  7. ('0x250e730', '0x250e7f0') 
  8.   
  9.   
  10. >>> gc.get_referents(a) 
  11. [
  12. >>> a.attr = b 
  13. >>> gc.get_referents(a) 
  14. [{'attr': <__main__.OBJ object at 0x0250E7F0>}, 
  15. >>> gc.get_referrers(b) 
  16. [{'attr': <__main__.OBJ object at 0x0250E7F0>}, {'a': <__main__.OBJ object at 0x0250E730>, 'b': <__main__.OBJ object at 0x0250E7F0>, 'OBJ': , '__builtins__': 
  17. le '__builtin__' (built-in)>, '__package__': None, 'gc': , '__name__': '__main__', '__doc__': None}] 
  18. >>>  

a, b都是類(lèi)OBJ的實(shí)例,執(zhí)行”a.attr = b”之后,a就通過(guò)‘’attr“這個(gè)屬性指向了b。

 
 
 
 
  1. gc.set_debug(flags) 

設(shè)置調(diào)試選項(xiàng),非常有用,常用的flag組合包含以下

gc.DEBUG_COLLETABLE: 打印可以被垃圾回收器回收的對(duì)象

gc.DEBUG_UNCOLLETABLE: 打印無(wú)法被垃圾回收器回收的對(duì)象,即定義了__del__的對(duì)象

gc.DEBUG_SAVEALL:當(dāng)設(shè)置了這個(gè)選項(xiàng),可以被拉起回收的對(duì)象不會(huì)被真正銷(xiāo)毀(free),而是放到gc.garbage這個(gè)列表里面,利于在線(xiàn)上查找問(wèn)題

內(nèi)存泄露

既然Python中通過(guò)引用計(jì)數(shù)和垃圾回收來(lái)管理內(nèi)存,那么什么情況下還會(huì)產(chǎn)生內(nèi)存泄露呢?有兩種情況:

第一是對(duì)象被另一個(gè)生命周期特別長(zhǎng)的對(duì)象所引用,比如網(wǎng)絡(luò)服務(wù)器,可能存在一個(gè)全局的單例ConnectionManager,管理所有的連接Connection,如果當(dāng)Connection理論上不再被使用的時(shí)候,沒(méi)有從ConnectionManager中刪除,那么就造成了內(nèi)存泄露。

第二是循環(huán)引用中的對(duì)象定義了__del__函數(shù),這個(gè)在《程序員必知的Python陷阱與缺陷列表》一文中有詳細(xì)介紹,簡(jiǎn)而言之,如果定義了__del__函數(shù),那么在循環(huán)引用中Python解釋器無(wú)法判斷析構(gòu)對(duì)象的順序,因此就不錯(cuò)處理。

在任何環(huán)境,不管是服務(wù)器,客戶(hù)端,內(nèi)存泄露都是非常嚴(yán)重的事情。

如果是線(xiàn)上服務(wù)器,那么一定得有監(jiān)控,如果發(fā)現(xiàn)內(nèi)存使用率超過(guò)設(shè)置的閾值則立即報(bào)警,盡早發(fā)現(xiàn)些許還有救。當(dāng)然,誰(shuí)也不希望在線(xiàn)上修復(fù)內(nèi)存泄露,這無(wú)疑是給行駛的汽車(chē)換輪子,因此盡量在開(kāi)發(fā)環(huán)境或者壓力測(cè)試環(huán)境發(fā)現(xiàn)并解決潛在的內(nèi)存泄露。在這里,發(fā)現(xiàn)問(wèn)題最為關(guān)鍵,只要發(fā)現(xiàn)了問(wèn)題,解決問(wèn)題就非常容易了,因?yàn)榘凑涨懊娴恼f(shuō)法,出現(xiàn)內(nèi)存泄露只有兩種情況,在第一種情況下,只要在適當(dāng)?shù)臅r(shí)機(jī)解除引用就可以了;在第二種情況下,要么不再使用__del__函數(shù),換一種實(shí)現(xiàn)方式,要么解決循環(huán)引用。

那么怎么查找哪里存在內(nèi)存泄露呢?武器就是兩個(gè)庫(kù):gc、objgraph

在上面已經(jīng)介紹了gc這個(gè)模塊,理論上,通過(guò)gc模塊能夠拿到所有的被garbage collector管理的對(duì)象,也能知道對(duì)象之間的引用和被引用關(guān)系,就可以畫(huà)出對(duì)象之間完整的引用關(guān)系圖。但事實(shí)上還是比較復(fù)雜的,因?yàn)樵谶@個(gè)過(guò)程中一不小心又會(huì)引入新的引用關(guān)系,所以,有好的輪子就直接用吧,那就是objgraph。

objgraph

objgraph的實(shí)現(xiàn)調(diào)用了gc的這幾個(gè)函數(shù):gc.get_objects(), gc.get_referents(), gc.get_referers(),然后構(gòu)造出對(duì)象之間的引用關(guān)系。objgraph的代碼和文檔都寫(xiě)得比較好,建議一讀。

下面先介紹幾個(gè)十分實(shí)用的API

 
 
 
 
  1. def count(typename) 

返回該類(lèi)型對(duì)象的數(shù)目,其實(shí)就是通過(guò)gc.get_objects()拿到所用的對(duì)象,然后統(tǒng)計(jì)指定類(lèi)型的數(shù)目。

 
 
 
 
  1. def by_type(typename) 

返回該類(lèi)型的對(duì)象列表。線(xiàn)上項(xiàng)目,可以用這個(gè)函數(shù)很方便找到一個(gè)單例對(duì)象

 
 
 
 
  1. def show_most_common_types(limits = 10) 

打印實(shí)例最多的前N(limits)個(gè)對(duì)象,這個(gè)函數(shù)非常有用。在《Python內(nèi)存優(yōu)化》一文中也提到,該函數(shù)能發(fā)現(xiàn)可以用slots進(jìn)行內(nèi)存優(yōu)化的對(duì)象

 
 
 
 
  1. def show_growth() 

統(tǒng)計(jì)自上次調(diào)用以來(lái)增加得最多的對(duì)象,這個(gè)函數(shù)非常有利于發(fā)現(xiàn)潛在的內(nèi)存泄露。函數(shù)內(nèi)部調(diào)用了gc.collect(),因此即使有循環(huán)引用也不會(huì)對(duì)判斷造成影響。

值得一提,該函數(shù)的實(shí)現(xiàn)非常有意思,簡(jiǎn)化后的代碼如下:

 
 
 
 
  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使用了可變參數(shù)作為默認(rèn)形參,這樣很方便記錄上一次的運(yùn)行結(jié)果。在《程序員必知的Python陷阱與缺陷列表》中提到,使用可變對(duì)象做默認(rèn)形參是最為常見(jiàn)的python陷阱,但在這里,卻成為了方便的利器!

 
 
 
 
  1. def show_backrefs() 

生產(chǎn)一張有關(guān)objs的引用圖,看出看出對(duì)象為什么不釋放,后面會(huì)利用這個(gè)API來(lái)查內(nèi)存泄露。

該API有很多有用的參數(shù),比如層數(shù)限制(max_depth)、寬度限制(too_many)、輸出格式控制(filename output)、節(jié)點(diǎn)過(guò)濾(filter, extra_ignore),建議使用之間看一些document。

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

找到一條指向obj對(duì)象的最短路徑,且路徑的頭部節(jié)點(diǎn)需要滿(mǎn)足predicate函數(shù) (返回值為T(mén)rue)

可以快捷、清晰指出 對(duì)象的被引用的情況,后面會(huì)展示這個(gè)函數(shù)的威力

 
 
 
 
  1. def show_chain(): 

將find_backref_chain 找到的路徑畫(huà)出來(lái), 該函數(shù)事實(shí)上調(diào)用show_backrefs,只是排除了所有不在路徑中的節(jié)點(diǎn)。

查找內(nèi)存泄露

在這一節(jié),介紹如何利用objgraph來(lái)查找內(nèi)存是怎么泄露的

如果我們懷疑一段代碼、一個(gè)模塊可能會(huì)導(dǎo)致內(nèi)存泄露,那么首先調(diào)用一次obj.show_growth(),然后調(diào)用相應(yīng)的函數(shù),最后再次調(diào)用obj.show_growth(),看看是否有增加的對(duì)象。比如下面這個(gè)簡(jiǎn)單的例子:

 
 
 
 
  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()  

運(yùn)行結(jié)果(我們只關(guān)心后一次show_growth的結(jié)果)如下

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

代碼很簡(jiǎn)單,函數(shù)開(kāi)始的時(shí)候講對(duì)象加入了global作用域的_cache列表,然后期望是在函數(shù)退出之前從_cache刪除,但是由于提前返回或者異常,并沒(méi)有執(zhí)行到最后的remove語(yǔ)句。從運(yùn)行結(jié)果可以發(fā)現(xiàn),調(diào)用函數(shù)之后,增加了一個(gè)類(lèi)OBJ的實(shí)例,然而理論上函數(shù)調(diào)用結(jié)束之后,所有在函數(shù)作用域(local)中聲明的對(duì)象都改被銷(xiāo)毀,因此這里就存在內(nèi)存泄露。

當(dāng)然,在實(shí)際的項(xiàng)目中,我們也不清楚泄露是在哪段代碼、哪個(gè)模塊中發(fā)生的,而且往往是發(fā)生了內(nèi)存泄露之后再去排查,這個(gè)時(shí)候使用obj.show_most_common_types就比較合適了,如果一個(gè)自定義的類(lèi)的實(shí)例數(shù)目特別多,那么就可能存在內(nèi)存泄露。如果在壓力測(cè)試環(huán)境,停止壓測(cè),調(diào)用gc.collet,然后再用obj.show_most_common_types查看,如果對(duì)象的數(shù)目沒(méi)有相應(yīng)的減少,那么肯定就是存在泄露。

當(dāng)我們定位了哪個(gè)對(duì)象發(fā)生了內(nèi)存泄露,那么接下來(lái)就是分析怎么泄露的,引用鏈?zhǔn)窃趺礃拥?,這個(gè)時(shí)候就該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查看內(nèi)存泄露

注意,上面的代碼中,max_depth參數(shù)非常關(guān)鍵,如果這個(gè)參數(shù)太小,那么看不到完整的引用鏈,如果這個(gè)參數(shù)太大,運(yùn)行的時(shí)候又非常耗時(shí)間。

然后打開(kāi)dot文件,結(jié)果如下

可以看到泄露的對(duì)象(紅框表示),是被一個(gè)叫_cache的list所引用,而_cache又是被__main__這個(gè)module所引用。

對(duì)于示例代碼,dot文件的結(jié)果已經(jīng)非常清晰,但是對(duì)于真實(shí)項(xiàng)目,引用鏈中的節(jié)點(diǎn)可能成百上千,看起來(lái)非常頭大,下面用tornado起一個(gè)最最簡(jiǎn)單的web服務(wù)器(代碼不知道來(lái)自哪里,且沒(méi)有內(nèi)存泄露,這里只是為了顯示引用關(guān)系),然后繪制socket的引用關(guān)關(guān)系圖,代碼和引用關(guān)系圖如下:

 
 
 
 
  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() 
  39.   
  40. tornado_server實(shí)例 

 

可見(jiàn),代碼越復(fù)雜,相互之間的引用關(guān)系越多,show_backrefs越難以看懂。這個(gè)時(shí)候就使用show_chain和find_backref_chain吧,這種方法,在官方文檔也是推薦的,我們稍微改改代碼,結(jié)果如下:

 
 
 
 
  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.     ) 

 

上面介紹了內(nèi)存泄露的第一種情況,對(duì)象被“非期望”地引用著。下面看看第二種情況,循環(huán)引用中的__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')  

上面的代碼存在循環(huán)引用,而且OBJ類(lèi)定義了__del__函數(shù)。如果沒(méi)有定義__del__函數(shù),那么上述的代碼會(huì)報(bào)錯(cuò), 因?yàn)間c.collect會(huì)將循環(huán)引用刪除,objgraph.by_type(‘OBJ’)返回空列表。而因?yàn)槎x了__del__函數(shù),gc.collect也無(wú)能為力,結(jié)果如下:

從圖中可以看到,對(duì)于這種情況,還是比較好辨識(shí)的,因?yàn)閛bjgraph將__del__函數(shù)用特殊顏色標(biāo)志出來(lái),一眼就看見(jiàn)了。另外,可以看見(jiàn)gc.garbage(類(lèi)型是list)也引用了這兩個(gè)對(duì)象,原因在document中有描述,當(dāng)執(zhí)行垃圾回收的時(shí)候,會(huì)將定義了__del__函數(shù)的類(lèi)實(shí)例(被稱(chēng)為uncollectable object)放到gc.garbage列表,因此,也可以直接通過(guò)查看gc.garbage來(lái)找出定義了__del__的循環(huán)引用。在這里,通過(guò)增加extra_ignore來(lái)排除gc.garbage的影響:

將上述代碼的最后一行改成:

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

 

另外,也可以設(shè)置DEBUG_UNCOLLECTABLE 選項(xiàng),直接將uncollectable對(duì)象輸出到標(biāo)準(zhǔn)輸出,而不是放到gc.garbage

循環(huán)引用

除非定義了__del__方法,那么循環(huán)引用也不是什么萬(wàn)惡不赦的東西,因?yàn)槔厥掌骺梢蕴幚硌h(huán)引用,而且不準(zhǔn)是python標(biāo)準(zhǔn)庫(kù)還是大量使用的第三方庫(kù),都可能存在循環(huán)引用。如果存在循環(huán)引用,那么Python的gc就必須開(kāi)啟(gc.isenabled()返回True),否則就會(huì)內(nèi)存泄露。但是在某些情況下,我們還是不希望有g(shù)c,比如對(duì)內(nèi)存和性能比較敏感的應(yīng)用場(chǎng)景,在這篇文章中,提到instagram通過(guò)禁用gc,性能提升了10%;另外,在一些應(yīng)用場(chǎng)景,垃圾回收帶來(lái)的卡頓也是不能接受的,比如RPG游戲。從前面對(duì)垃圾回收的描述可以看到,執(zhí)行一次垃圾回收是很耗費(fèi)時(shí)間的,因?yàn)樾枰闅v所有被collector管理的對(duì)象(即使很多對(duì)象不屬于垃圾)。因此,要想禁用GC,就得先徹底干掉循環(huán)引用。

同內(nèi)存泄露一樣,解除循環(huán)引用的前提是定位哪里出現(xiàn)了循環(huán)引用。而且,如果需要在線(xiàn)上應(yīng)用關(guān)閉gc,那么需要自動(dòng)、持久化的進(jìn)行檢測(cè)。下面介紹如何定位循環(huán)引用,以及如何解決循環(huán)引用。

定位循環(huán)引用

這里還是是用GC模塊和objgraph來(lái)定位循環(huán)引用。需要注意的事,一定要先禁用gc(調(diào)用gc.disable()), 防止誤差。

這里利用之前介紹循環(huán)引用時(shí)使用過(guò)的例子: a, b兩個(gè)OBJ對(duì)象形成循環(huán)引用

 
 
 
 
  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)  

運(yùn)行結(jié)果(部分):

 
 
 
 
  1. wrapper_descriptor 1060 
  2.  
  3. dict 555 
  4.  
  5. OBJ 100  

上面的代碼中使用的是show_most_common_types,而沒(méi)有使用show_growth(因?yàn)間rowth會(huì)手動(dòng)調(diào)用gc.collect()),通過(guò)結(jié)果可以看到,內(nèi)存中現(xiàn)在有100個(gè)OBJ對(duì)象,符合預(yù)期。當(dāng)然這些OBJ對(duì)象沒(méi)有在函數(shù)調(diào)用后被銷(xiāo)毀,不一定是循環(huán)引用的問(wèn)題,也可能是內(nèi)存泄露,比如前面OBJ對(duì)象被global作用域中的_cache引用的情況。怎么排除是否是被global作用域的變量引用的情況呢,方法還是objgraph.find_backref_chain(obj),在__doc__中指出,如果找不到符合條件的應(yīng)用鏈(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  

上面的代碼輸出:

 
 
 
 
  1. [<__main__.OBJ object at 0x0244F810>] 

驗(yàn)證了我們的想法,OBJ對(duì)象不是被global作用域的變量所引用。

在實(shí)際項(xiàng)目中,不大可能到處用objgraph.show_most_common_types或者objgraph.by_type來(lái)排查循環(huán)引用,效率太低。有沒(méi)有更好的辦法呢,有的,那就是使用gc模塊的debug 選項(xiàng)。在前面介紹gc模塊的時(shí)候,就介紹了gc.DEBUG_COLLECTABLE 選項(xiàng),我們來(lái)試試:

 
 
 
 
  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事實(shí)上無(wú)所謂 
  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行設(shè)置了debug flag,可以打印出collectable對(duì)象。另外,只用調(diào)用一次show_cycle_reference函數(shù)就足夠了(這也比objgraph.show_most_common_types方便一點(diǎn))。在第16行手動(dòng)調(diào)用gc.collect(),輸出如下:

 
 
 
 
  1. gc: collectable  
  2.  
  3. gc: collectable  
  4.  
  5. gc: collectable  
  6.  
  7. gc: collectable   

注意:只有當(dāng)對(duì)象是unreachable且collectable的時(shí)候,在collect的時(shí)候才會(huì)被輸出,也就是說(shuō),如果是reachable,比如被global作用域的變量引用,那么也是不會(huì)輸出的。

通過(guò)上面的輸出,我們已經(jīng)知道OBJ類(lèi)的實(shí)例存在循環(huán)引用,但是這個(gè)時(shí)候,obj實(shí)例已經(jīng)被回收了。那么如果我想通過(guò)show_backrefs找出這個(gè)引用關(guān)系,需要重新調(diào)用show_cycle_reference函數(shù),然后不調(diào)用gc.collect,通過(guò)show_backrefs 和 by_type繪制。有沒(méi)有更好的辦法呢,可以讓我在一次運(yùn)行中發(fā)現(xiàn)循環(huán)引用,并找出引用鏈?答案就是使用DEBUG_SAVEALL,下面為了展示方便,直接在命令行中操作(當(dāng)然,使用ipython更好)

 
 
 
 
  1. >>> import gc, objgraph 
  2. >>> class OBJ(object): 
  3. ... pass 
  4. ... 
  5. >>> def show_cycle_reference(): 
  6. ... a, b = OBJ(), OBJ() 
  7. ... a.attr_b = b 
  8. ... b.attr_a = a 
  9. ... 
  10. >>> gc.set_debug(gc.DEBUG_SAVEALL| gc.DEBUG_OBJECTS) 
  11. >>> show_cycle_reference() 
  12. >>> print 'before collect', gc.garbage 
  13. before collect [] 
  14. >>> print gc.collect() 
  15. >>> 
  16. >>> for o in gc.garbage: 
  17. ... print o 
  18. ... 
  19. <__main__.OBJ object at 0x024BB7D0> 
  20. <__main__.OBJ object at 0x02586850> 
  21. {'attr_b': <__main__.OBJ object at 0x02586850>} 
  22. {'attr_a': <__main__.OBJ object at 0x024BB7D0>} 
  23. >>> 
  24. >>> objgraph.show_backrefs(objgraph.at(0x024BB7D0), 5, filename = 'obj.dot') 
  25. Graph written to obj.dot (13 nodes) 
  26. >>>  

上面在調(diào)用gc.collect之前,gc.garbage里面是空的,由于設(shè)置了DEBUG_SAVEALL,那么調(diào)用gc.collect時(shí),會(huì)將collectable對(duì)象放到gc.garbage。此時(shí),對(duì)象沒(méi)有被釋放,我們就可以直接繪制出引用關(guān)系,這里使用了objgraph.at,當(dāng)然也可以使用objgraph.by_type, 或者直接從gc.garbage取對(duì)象,結(jié)果如下:

出了循環(huán)引用,可以看見(jiàn)還有兩個(gè)引用,gc.garbage與局部變量o,相信大家也能理解。

消滅循環(huán)引用

找到循環(huán)引用關(guān)系之后,解除循環(huán)引用就不是太難的事情,總的來(lái)說(shuō),有兩種辦法:手動(dòng)解除與使用weakref。

手動(dòng)解除很好理解,就是在合適的時(shí)機(jī),解除引用關(guān)系。比如,前面提到的collections.OrderedDict:

 
 
 
 
  1. >>> root = [] 
  2. >>> root[:] = [root, root, None] 
  3. >>> 
  4. >>> root 
  5. [[...], [...], None] 
  6. >>> 
  7. >>> del root[:] 
  8. >>> root 
  9. []  

更常見(jiàn)的情況,是我們自定義的對(duì)象之間存在循環(huán)引用:要么是單個(gè)對(duì)象內(nèi)的循環(huán)引用,要么是多個(gè)對(duì)象間的循環(huán)引用,我們看一個(gè)單個(gè)對(duì)象內(nèi)循環(huán)引用的例子:

 
 
 
 
  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, *a
    當(dāng)前名稱(chēng):使用GC、Objgraph干掉Python內(nèi)存泄露與循環(huán)引用!
    網(wǎng)頁(yè)地址:http://www.5511xx.com/article/dphosep.html