最近在編寫(xiě)偏微分方程反問(wèn)題的MCMC采樣算法時(shí)需要1e5-1e6次方的大量迭代白嘁,發(fā)現(xiàn)隨著迭代的進(jìn)行但惶,16G內(nèi)存在迭代到1e5左右時(shí)就會(huì)消耗完枷遂,導(dǎo)致Python程序自動(dòng)退出澈灼。仔細(xì)觀察所寫(xiě)的程序竞川,感覺(jué)不應(yīng)有這個(gè)問(wèn)題,循環(huán)中舊的變量被新的變量應(yīng)該覆蓋!經(jīng)仔細(xì)分析發(fā)現(xiàn)FEniCS軟件包的使用導(dǎo)致了內(nèi)存泄漏流译,在循環(huán)中反復(fù)用到了
import fenics as fe
fun = fe.Function(function_space)
不斷的在函數(shù)調(diào)用里面每次生成一個(gè)FEniCS中的函數(shù)逞怨,但經(jīng)實(shí)際測(cè)試,即使退出了函數(shù)福澡,使用
del fun
均無(wú)法釋放內(nèi)存叠赦!
通過(guò)查詢(xún)Python編成可能遇到的內(nèi)存泄漏問(wèn)題:
- Numpy使用造成的,詳見(jiàn):https://zhuanlan.zhihu.com/p/80689571
- List等變量的指針指向的內(nèi)存空間沒(méi)有被釋放革砸,詳細(xì)如下:
Py的一個(gè)大好處除秀,就是靈活的變量聲明和動(dòng)態(tài)變量類(lèi)型。雖然這使得學(xué)習(xí)py起來(lái)非常方便快捷算利,但是同時(shí)也帶來(lái)了py在性能上的一些不足册踩。其中相關(guān)內(nèi)存比較主要的一點(diǎn)就是py不會(huì)對(duì)已經(jīng)銷(xiāo)毀的對(duì)象所占據(jù)的內(nèi)存做自動(dòng)的釋放內(nèi)存空間的工作。
在細(xì)看內(nèi)存釋放工作之前效拭,有必要先來(lái)了解一下py的垃圾回收機(jī)制暂吉。Python中,主要依靠gc(garbage collector)模塊的引用計(jì)數(shù)技術(shù)來(lái)進(jìn)行垃圾回收缎患。所謂引用計(jì)數(shù)慕的,就是考慮到Python中變量的本質(zhì)不是內(nèi)存中一塊存儲(chǔ)數(shù)據(jù)的區(qū)域,而是對(duì)一塊內(nèi)存數(shù)據(jù)區(qū)域的引用挤渔。所以python可以給所有的對(duì)象(內(nèi)存中的區(qū)域)維護(hù)一個(gè)引用計(jì)數(shù)的屬性肮街,在一個(gè)引用被創(chuàng)建或復(fù)制的時(shí)候,讓python,把相關(guān)對(duì)象的引用計(jì)數(shù)+1判导;相反當(dāng)引用被銷(xiāo)毀的時(shí)候就把相關(guān)對(duì)象的引用計(jì)數(shù)-1嫉父。當(dāng)對(duì)象的引用計(jì)數(shù)減到0時(shí),自然就可以認(rèn)為整個(gè)python中不會(huì)再有變量引用這個(gè)對(duì)象眼刃,所以就可以把這個(gè)對(duì)象所占據(jù)的內(nèi)存空間釋放出來(lái)了绕辖。
引用計(jì)數(shù)技術(shù)在每次引用創(chuàng)建和銷(xiāo)毀時(shí)都要多做一些操作,這可能是一個(gè)小缺點(diǎn)擂红,當(dāng)創(chuàng)建和銷(xiāo)毀很頻繁的時(shí)候難免帶來(lái)一些效率上的不足引镊。但是其最大的好處就是實(shí)時(shí)性,其他語(yǔ)言當(dāng)中篮条,垃圾回收可能只能在一些固定的時(shí)間點(diǎn)上進(jìn)行,比如當(dāng)內(nèi)存分配失敗的時(shí)候進(jìn)行垃圾回收吩抓,而引用計(jì)數(shù)技術(shù)可以動(dòng)態(tài)地進(jìn)行內(nèi)存的管理涉茧。
如果說(shuō)效率只是一個(gè)不足的話,那么引用計(jì)數(shù)存在一些比較致命的軟肋使得其一直不被接受為一種可以廣泛運(yùn)用的垃圾回收機(jī)制疹娶,這便是對(duì)循環(huán)引用的處理伴栓。在Python中有一些類(lèi)型比如tuple,list,dict等,其作為容器類(lèi)型可以包含若干個(gè)對(duì)象。如果某個(gè)對(duì)象就是它本身钳垮,或者兩個(gè)對(duì)象中互相包含對(duì)方惑淳,那么就構(gòu)成了一個(gè)循環(huán)引用。比如下面這段代碼:
import sys
class Test():
def __init__(self):
pass
t = Test()
k = Test()
t._self = t
print sys.getrefcount(t) #sys.getrefcount函數(shù)用來(lái)查看一個(gè)對(duì)象有幾個(gè)引用
print sys.getrefcount(k)
####結(jié)果####
3
2
getrefcount函數(shù)查看一個(gè)對(duì)象存在幾個(gè)引用關(guān)系饺窿,一般狀態(tài)下的普通變量如上面的k歧焦,返回值都是2。不是1是因?yàn)榘裬作為參數(shù)傳遞給函數(shù)的時(shí)候肚医,要先復(fù)制一份引用绢馍,然后把這個(gè)引用賦給形式參數(shù)供函數(shù)運(yùn)行,在函數(shù)運(yùn)行過(guò)程中肠套,會(huì)保持這個(gè)引用始終升高為2舰涌。從上面運(yùn)行的結(jié)果可以看出來(lái),Test類(lèi)實(shí)例t由于添加了一個(gè)自己對(duì)自己的引用你稚,相當(dāng)于:
1119804-20170809183056839-100581675.png
del語(yǔ)句可以消除一個(gè)引用關(guān)系瓷耙。對(duì)于沒(méi)有_self這樣的自我引用的情況下,del(k)相當(dāng)于銷(xiāo)毀了變量名到內(nèi)存地址的這一層引用關(guān)系刁赖,自getrefcount執(zhí)行完成之后搁痛,這部分內(nèi)存就可以得到釋放了。但是如果存在_self這個(gè)自我引用的話乾闰,即使消除了del(t)這個(gè)引用關(guān)系落追,這個(gè)對(duì)象的引用計(jì)數(shù)仍然是1。得不到銷(xiāo)毀涯肩,所以會(huì)造成內(nèi)存泄露轿钠。可以看到病苗,基于引用計(jì)數(shù)的垃圾回收機(jī)制因?yàn)檠h(huán)引用的存在可能會(huì)導(dǎo)致內(nèi)存泄露疗垛,所以python在引用計(jì)數(shù)的基礎(chǔ)上也增加了其他幾種垃圾回收的方式。這里簡(jiǎn)單提一下硫朦。
標(biāo)記-清除的回收機(jī)制: 針對(duì)循環(huán)引用這個(gè)問(wèn)題贷腕,比如有兩個(gè)對(duì)象互相引用了對(duì)方,當(dāng)外界沒(méi)有對(duì)他們有任何引用咬展,也就是說(shuō)他們各自的引用計(jì)數(shù)都只有1的時(shí)候泽裳,如果可以識(shí)別出這個(gè)循環(huán)引用,把它們屬于循環(huán)的計(jì)數(shù)減掉的話破婆,就可以看到他們的真實(shí)引用計(jì)數(shù)了涮总。基于這樣一種考慮祷舀,有一種方法瀑梗,比如從對(duì)象A出發(fā)烹笔,沿著引用尋找到對(duì)象B,把對(duì)象B的引用計(jì)數(shù)減去1抛丽;然后沿著B(niǎo)對(duì)A的引用回到A谤职,把A的引用計(jì)數(shù)減1,這樣就可以把這層循環(huán)引用關(guān)系給去掉了亿鲜。不過(guò)這么做還有一個(gè)考慮不周的地方允蜈。假如A對(duì)B的引用是單向的, 在到達(dá)B之前我不知道B是否也引用了A狡门,這樣子先給B減1的話就會(huì)使得B稱(chēng)為不可達(dá)的對(duì)象了陷寝。為了解決這個(gè)問(wèn)題,python中常常把內(nèi)存塊一分為二其馏,將一部分用于保存真的引用計(jì)數(shù)凤跑,另一部分拿來(lái)做為一個(gè)引用計(jì)數(shù)的副本,在這個(gè)副本上做一些實(shí)驗(yàn)叛复。比如在副本中維護(hù)兩張鏈表仔引,一張里面放不可被回收的對(duì)象合集,另一張里面放被標(biāo)記為可以被回收(計(jì)數(shù)經(jīng)過(guò)上面所說(shuō)的操作減為0)的對(duì)象褐奥,然后再到后者中找一些被前者表中一些對(duì)象直接或間接單向引用的對(duì)象咖耘,把這些移動(dòng)到前面的表里面。這樣就可以讓不應(yīng)該被回收的對(duì)象不會(huì)被回收撬码,應(yīng)該被回收的對(duì)象都被回收了儿倒。
分代回收: 代回收策略著眼于提升垃圾回收的效率。研究表明呜笑,任何語(yǔ)言夫否,任何環(huán)境的編程中,對(duì)于變量在內(nèi)存中的創(chuàng)建/銷(xiāo)毀叫胁,總有頻繁和不那么頻繁的凰慈。比如任何程序中總有生命周期是全局的、部分的變量驼鹅。而在垃圾回收的過(guò)程中微谓,其實(shí)在進(jìn)行垃圾回收之前還要進(jìn)行一步垃圾檢測(cè),即檢查某個(gè)對(duì)象是不是垃圾输钩,該不該被回收豺型。當(dāng)對(duì)象很多,垃圾檢測(cè)將耗費(fèi)大量的時(shí)間而真的垃圾回收花不了多久买乃。對(duì)于這種多對(duì)象程序触创,我們可以把一些進(jìn)行垃圾回收頻率相近的對(duì)象稱(chēng)為“同一代”的對(duì)象。垃圾檢測(cè)的時(shí)候可以對(duì)頻率較高的“代”多檢測(cè)幾次为牍,反之哼绑,進(jìn)行垃圾回收頻率較低的“代”可以少檢測(cè)幾次。這樣就可以提高垃圾回收的效率了碉咆。至于如何判斷一個(gè)對(duì)象屬于什么代抖韩,python中采取的方法是通過(guò)其生存時(shí)間來(lái)判斷。如果在好幾次垃圾檢測(cè)中疫铜,該變量都是reachable的話茂浮,那就說(shuō)明這個(gè)變量越不是垃圾,就要把這個(gè)變量往高的代移動(dòng)壳咕,要減少對(duì)其進(jìn)行垃圾檢測(cè)的頻率席揽。
gc模塊的介紹: 根據(jù)以上的介紹,我們知道了python對(duì)于垃圾回收谓厘,采取的是引用計(jì)數(shù)為主幌羞,標(biāo)記-清除+分代回收為輔的回收策略。對(duì)于循環(huán)引用的情況竟稳,一般的自動(dòng)垃圾回收方式肯定是無(wú)效了属桦,這時(shí)候就需要顯式地調(diào)用一些操作來(lái)保證垃圾的回收和內(nèi)存不泄露。這就要用到python內(nèi)建的垃圾回收模塊gc模塊了他爸。最常見(jiàn)的gc模塊的使用就是用gc.collect()方法聂宾。那就先來(lái)看下這個(gè)方法把:
import sys
import gc
a = [1]
b = [2]
a.append(b)
b.append(a)
####此時(shí)a和b之間存在循環(huán)引用####
sys.getrefcount(a) #結(jié)果應(yīng)該是3
sys.getrefcount(b) #結(jié)果應(yīng)該是3
del a
del b
####刪除了變量名a,b到對(duì)象的引用诊笤,此時(shí)引用計(jì)數(shù)應(yīng)該減為1系谐,即只剩下互相引用了####
try:
sys.getrefcount(a)
except UnboundLocalError:
print 'a is invalid'
####此時(shí),原來(lái)a指向的那個(gè)對(duì)象引用不為0讨跟,python不會(huì)自動(dòng)回收它的內(nèi)存空間####
####但是我們又沒(méi)辦法通過(guò)變量名a來(lái)引用它了纪他,這就導(dǎo)致了內(nèi)存泄露####
unreachable_count = gc.collect()
####gc.collect()專(zhuān)門(mén)用來(lái)處理這些循環(huán)引用,返回處理這些循環(huán)引用一共釋放掉的對(duì)象個(gè)數(shù)许赃。這里返回是2####
可以看到止喷,沒(méi)有g(shù)c模塊的時(shí)候,我們對(duì)循環(huán)引用是束手無(wú)策的混聊,在調(diào)用了一些gc模塊的方法之后弹谁,它會(huì)實(shí)現(xiàn)上面“垃圾回收機(jī)制”部分中提到的一些策略比如“標(biāo)記-清除”來(lái)進(jìn)行垃圾回收。因?yàn)橛辛诉@個(gè)模塊的封裝句喜,我們就不用關(guān)心具體的實(shí)現(xiàn)了预愤。然而collect方法也不是萬(wàn)能的。有些時(shí)候它并不能有效地回收所有該回收的對(duì)象咳胃。比如下面這樣一段代碼:
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() #結(jié)果是4
print gc.garbage #結(jié)果是[<__main__.A instance at 0x0000000002296448>, <__main__.B instance at 0x0000000002296488>]
可以看到植康,對(duì)我們自定義類(lèi)的對(duì)象而言,collect方法并不能解決循環(huán)引用引起的內(nèi)存泄露展懈,即使在collect過(guò)后销睁,解釋器中仍然存在兩個(gè)垃圾對(duì)象供璧。
這里需要明確一下,之前對(duì)于“垃圾”二字的定義并不是很明確冻记,在這里的這個(gè)語(yǔ)境下睡毒,垃圾是指在經(jīng)過(guò)collect的垃圾回收之后仍然保持unreachable狀態(tài),即無(wú)法被回收冗栗,且無(wú)法被用戶調(diào)用的對(duì)象應(yīng)該叫做垃圾演顾。gc模塊中有g(shù)arbage這個(gè)屬性,其為一個(gè)列表隅居,每一項(xiàng)都是當(dāng)前解釋器中存在的垃圾對(duì)象钠至。一般情況下,這個(gè)屬性始終保持為空集胎源。
那么為什么在這種場(chǎng)景下collect不起作用了呢棉钧?這主要是因?yàn)槲覀冊(cè)陬?lèi)中重載了del方法。del方法指出了在用del語(yǔ)句刪除對(duì)象時(shí)除了釋放內(nèi)存空間以外的操作乒融。一般而言掰盘,在使用了del語(yǔ)句的時(shí)候解釋器會(huì)首先看要?jiǎng)h除對(duì)象的引用計(jì)數(shù),如果為0赞季,那么就釋放內(nèi)存并執(zhí)行del方法愧捕。在這里,首先del語(yǔ)句出現(xiàn)時(shí)本身引用計(jì)數(shù)就不為0(因?yàn)橛醒h(huán)引用的存在)申钩,所以解釋器不釋放內(nèi)存次绘;再者,執(zhí)行collect方法時(shí)照理由應(yīng)該會(huì)清除循環(huán)引用所產(chǎn)生的無(wú)效引用計(jì)數(shù)從而達(dá)到del的目的撒遣,對(duì)于這兩個(gè)對(duì)象而言邮偎,python無(wú)法判斷調(diào)用它們的del方法時(shí)會(huì)不會(huì)要用到對(duì)方那個(gè)對(duì)象,比如在進(jìn)行b.del()時(shí)可能會(huì)用到b._a也就是a义黎,如果在那之前a已經(jīng)被釋放禾进,那么就徹底GG了。為了避免這種情況廉涕,collect方法默認(rèn)不對(duì)重載了del方法的循環(huán)引用對(duì)象進(jìn)行回收泻云,而它們倆的狀態(tài)也會(huì)從unreachable轉(zhuǎn)變?yōu)閡ncollectable。由于是uncollectable的狐蜕,自然就不會(huì)被collect處理宠纯,所以就進(jìn)入了garbage列表。
collect返回4的原因是因?yàn)椴闶停贏和B類(lèi)對(duì)象中還默認(rèn)有一個(gè)dict屬性婆瓜,里面有所有屬性的信息。比如對(duì)于a贡羔,有a.dict = {'_b':<main.B instance at xxxxxxxx>}廉白。a的dict和b的dict也是循環(huán)引用的个初。但是字典類(lèi)型不涉及自定義的del方法,所以可以被collect掉蒙秒。所以garbage里只剩下兩個(gè)了勃黍。
有時(shí)候garbage里也會(huì)出現(xiàn)那兩個(gè)dict,這主要是因?yàn)樵谇懊婵赡茉O(shè)置了gc模塊的debug模式晕讲,比如gc.set_debug(gc.DEBUG_LEAK),會(huì)把所有已經(jīng)回收掉的unreachable的對(duì)象也都加入到garbage里面马澈。set_debug還有很多參數(shù)諸如gc.DEBUG_STAT|DEBUG_COLLECTABLE|DEBUG_UNCOLLECTABLE|DEBUG_SAVEALL等等瓢省,設(shè)置了相關(guān)參數(shù)后gc模塊會(huì)自動(dòng)檢測(cè)垃圾回收狀況并給出實(shí)時(shí)地信息反映。
gc.get_threshold(): 這個(gè)方法涉及到之前說(shuō)過(guò)的分代回收的策略痊班。python中默認(rèn)把所有對(duì)象分成三代勤婚。第0代包含了最新的對(duì)象,第2代則是最早的一些對(duì)象涤伐。在一次垃圾回收中馒胆,所有未被回收的對(duì)象會(huì)被移到高一代的地方。這個(gè)方法返回的是(700,10,10)凝果,這也是gc的默認(rèn)值祝迂。這個(gè)值的意思是說(shuō),在第0代對(duì)象數(shù)量達(dá)到700個(gè)之前器净,不把未被回收的對(duì)象放入第一代型雳;而在第一代對(duì)象數(shù)量達(dá)到10個(gè)之前也不把未被回收的對(duì)象移到第二代∩胶Γ可以是使用gc.set_threshold(threashold0,threshold1,threshold2)來(lái)手動(dòng)設(shè)置這組閾值纠俭。