Python的內(nèi)存泄漏及gc模塊的使用分析
一般來說在 Python 中,為了解決內(nèi)存泄漏問題,采用了對(duì)象引用計(jì)數(shù),并基于引用計(jì)數(shù)實(shí)現(xiàn)自動(dòng)垃圾回收。
由于Python 有了自動(dòng)垃圾回收功能,就造成了不少初學(xué)者誤認(rèn)為自己從此過上了好日子,不必再受內(nèi)存泄漏的騷擾了。但如果仔細(xì)查看一下Python文檔對(duì) __del__() 函數(shù)的描述,就知道這種好日子里也是有陰云的。下面摘抄一點(diǎn)文檔內(nèi)容如下:
Some common situations that may prevent the reference count of an object from going to zero include: circular references between objects (e.g., a doubly-linked list or a tree data structure with parent and child pointers); a reference to the object on the stack frame of a function that caught an exception (the traceback stored in sys.exc_traceback keeps the stack frame alive); or a reference to the object on the stack frame that raised an unhandled exception in interactive mode (the traceback stored in sys.last_traceback keeps the stack frame alive).
可見,有 __del__() 函數(shù)的對(duì)象間的循環(huán)引用是導(dǎo)致內(nèi)存泄漏的主兇。
另外需要說明:對(duì)沒有 __del__() 函數(shù)的 Python 對(duì)象間的循環(huán)引用,是可以被自動(dòng)垃圾回收掉的。
如何知道一個(gè)對(duì)象是否內(nèi)存泄漏了呢?
方法一、當(dāng)你認(rèn)為一個(gè)對(duì)象應(yīng)該被銷毀時(shí)(即引用計(jì)數(shù)為 0),可以通過 sys.getrefcount(obj) 來獲取對(duì)象的引用計(jì)數(shù),并根據(jù)返回值是否為 0 來判斷是否內(nèi)存泄漏。如果返回的引用計(jì)數(shù)不為 0,說明在此刻對(duì)象 obj 是不能被垃圾回收器回收掉的。
方法二、也可以通過 Python 擴(kuò)展模塊 gc 來查看不能回收的對(duì)象的詳細(xì)信息。
首先,來看一段正常的測(cè)試代碼:
#--------------- code begin -------------- # -*- coding: utf-8 -*- import gc import sys class CGcLeak(object): def __init__(self): self._text = '#'*10 def __del__(self): pass def make_circle_ref(): _gcleak = CGcLeak() # _gcleak._self = _gcleak # test_code_1 print '_gcleak ref count0:%d' % sys.getrefcount(_gcleak) del _gcleak try: print '_gcleak ref count1:%d' % sys.getrefcount(_gcleak) except UnboundLocalError: print '_gcleak is invalid!' def test_gcleak(): # Enable automatic garbage collection. gc.enable() # Set the garbage collection debugging flags. gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | / gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS) print 'begin leak test...' make_circle_ref() print 'begin collect...' _unreachable = gc.collect() print 'unreachable object num:%d' % _unreachable print 'garbage object num:%d' % len(gc.garbage) if __name__ == '__main__': test_gcleak()
在 test_gcleak() 中,設(shè)置垃圾回收器調(diào)試標(biāo)志后,再用 collect() 進(jìn)行垃圾回收,最后打印出該次垃圾回收發(fā)現(xiàn)的不可達(dá)的垃圾對(duì)象數(shù)和整個(gè)解釋器中的垃圾對(duì)象數(shù)。
gc.garbage 是一個(gè) list 對(duì)象,列表項(xiàng)是垃圾收集器發(fā)現(xiàn)的不可達(dá)(即是垃圾對(duì)象)、但又不能釋放(即不能回收)的對(duì)象。文檔描述為:A list of objects which the collector found to be unreachable but could not be freed (uncollectable objects).
通常,gc.garbage 中的對(duì)象是引用環(huán)中的對(duì)象。因?yàn)?Python 不知道按照什么樣的安全次序來調(diào)用環(huán)中對(duì)象的 __del__() 函數(shù),導(dǎo)致對(duì)象始終存活在 gc.garbage 中,造成內(nèi)存泄漏。如果知道一個(gè)安全的次序,那么就打破引用環(huán),再執(zhí)行 del gc.garbage[:] ,以清空垃圾對(duì)象列表。
上段代碼輸出為(#后字符串為筆者所加注釋):
#----------------------------------------- begin leak test... # 變量 _gcleak 的引用計(jì)數(shù)為 2. _gcleak ref count0:2 # _gcleak 變?yōu)椴豢蛇_(dá)(unreachable)的非法變量. _gcleak is invalid! # 開始垃圾回收 begin collect... # 本次垃圾回收發(fā)現(xiàn)的不可達(dá)的垃圾對(duì)象數(shù)為 0. unreachable object num:0 # 整個(gè)解釋器中的垃圾對(duì)象數(shù)為 0. garbage object num:0 #-----------------------------------------
由此可見 _gcleak 對(duì)象的引用計(jì)數(shù)是正確的,也沒有任何對(duì)象發(fā)生內(nèi)存泄漏。
如果不注釋掉 make_circle_ref() 中的 test_code_1 語句:
_gcleak._self = _gcleak
也就是讓 _gcleak 形成一個(gè)自己對(duì)自己的循環(huán)引用。再運(yùn)行上述代碼,輸出結(jié)果就變成:
#----------------------------------------- begin leak test... _gcleak ref count0:3 _gcleak is invalid! begin collect... # 發(fā)現(xiàn)可以回收的垃圾對(duì)象: 地址為 012AA090,類型為 CGcLeak. gc: uncollectable <CGcLeak 012AA090> gc: uncollectable <dict 012AC1E0> unreachable object num:2 #!! 不能回收的垃圾對(duì)象數(shù)為 1,導(dǎo)致內(nèi)存泄漏! garbage object num:1 #-----------------------------------------
可見 <CGcLeak 012AA090> 對(duì)象發(fā)生了內(nèi)存泄漏??!而多出的 dict 垃圾就是泄漏的 _gcleak 對(duì)象的字典,打印出字典信息為:
{'_self': <__main__.CGcLeak object at 0x012AA090>, '_text': '##########'}
除了對(duì)自己的循環(huán)引用,多個(gè)對(duì)象間的循環(huán)引用也會(huì)導(dǎo)致內(nèi)存泄漏。簡(jiǎn)單舉例如下:
#--------------- code begin -------------- class CGcLeakA(object): def __init__(self): self._text = '#'*10 def __del__(self): pass class CGcLeakB(object): def __init__(self): self._text = '*'*10 def __del__(self): pass def make_circle_ref(): _a = CGcLeakA() _b = CGcLeakB() _a._b = _b # test_code_2 _b._a = _a # test_code_3 print 'ref count0:a=%d b=%d' % / (sys.getrefcount(_a), sys.getrefcount(_b)) # _b._a = None # test_code_4 del _a del _b try: print 'ref count1:a=%d' % sys.getrefcount(_a) except UnboundLocalError: print '_a is invalid!' try: print 'ref count2:b=%d' % sys.getrefcount(_b) except UnboundLocalError: print '_b is invalid!' #--------------- code end ----------------
這次測(cè)試后輸出結(jié)果為:
#----------------------------------------- begin leak test... ref count0:a=3 b=3 _a is invalid! _b is invalid! begin collect... gc: uncollectable <CGcLeakA 012AA110> gc: uncollectable <CGcLeakB 012AA0B0> gc: uncollectable <dict 012AC1E0> gc: uncollectable <dict 012AC0C0> unreachable object num:4 garbage object num:2 #-----------------------------------------
可見 _a,_b 對(duì)象都發(fā)生了內(nèi)存泄漏。因?yàn)槎呤茄h(huán)引用,垃圾回收器不知道該如何回收,也就是不知道該首先調(diào)用那個(gè)對(duì)象的 __del__() 函數(shù)。
采用以下任一方法,打破環(huán)狀引用,就可以避免內(nèi)存泄漏:
1.注釋掉 make_circle_ref() 中的 test_code_2 語句;
2.注釋掉 make_circle_ref() 中的 test_code_3 語句;
3.取消對(duì) make_circle_ref() 中的 test_code_4 語句的注釋。
相應(yīng)輸出結(jié)果變?yōu)椋?/p>
#----------------------------------------- begin leak test... ref count0:a=2 b=3 # 注:此處輸出結(jié)果視情況變化. _a is invalid! _b is invalid! begin collect... unreachable object num:0 garbage object num:0 #-----------------------------------------
結(jié)論:Python 的 gc 有比較強(qiáng)的功能,比如設(shè)置 gc.set_debug(gc.DEBUG_LEAK) 就可以進(jìn)行循環(huán)引用導(dǎo)致的內(nèi)存泄露的檢查。如果在開發(fā)時(shí)進(jìn)行內(nèi)存泄露檢查;在發(fā)布時(shí)能夠確保不會(huì)內(nèi)存泄露,那么就可以延長(zhǎng) Python 的垃圾回收時(shí)間間隔、甚至主動(dòng)關(guān)閉垃圾回收機(jī)制,從而提高運(yùn)行效率。
- 10種檢測(cè)Python程序運(yùn)行時(shí)間、CPU和內(nèi)存占用的方法
- 談?wù)勅绾问謩?dòng)釋放Python的內(nèi)存
- python中查看變量?jī)?nèi)存地址的方法
- python清除函數(shù)占用的內(nèi)存方法
- python中使用psutil查看內(nèi)存占用的情況
- Python內(nèi)存泄漏和內(nèi)存溢出的解決方案
- Python進(jìn)程間通信之共享內(nèi)存詳解
- python 實(shí)時(shí)得到cpu和內(nèi)存的使用情況方法
- 淺談Python 對(duì)象內(nèi)存占用
- Python使用內(nèi)存緩存實(shí)例分享
相關(guān)文章
Python基于time模塊求程序運(yùn)行時(shí)間的方法
這篇文章主要介紹了Python基于time模塊求程序運(yùn)行時(shí)間的方法,涉及Python time模塊的使用及數(shù)值運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-09-09基于Tensorflow讀取MNIST數(shù)據(jù)集時(shí)網(wǎng)絡(luò)超時(shí)的解決方式
這篇文章主要介紹了基于Tensorflow讀取MNIST數(shù)據(jù)集時(shí)網(wǎng)絡(luò)超時(shí)的解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-06-06python多線程案例之多任務(wù)copy文件完整實(shí)例
這篇文章主要介紹了python多線程案例之多任務(wù)copy文件,結(jié)合完整實(shí)例形式分析了Python使用multiprocessing模塊實(shí)現(xiàn)基于多線程的文件拷貝相關(guān)操作技巧,需要的朋友可以參考下2019-10-10Python+Appium實(shí)現(xiàn)自動(dòng)化清理微信僵尸好友的方法
這篇文章主要介紹了Python+Appium實(shí)現(xiàn)自動(dòng)化清理微信僵尸好友的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02python 下劃線的多種應(yīng)用場(chǎng)景總結(jié)
Python有很多地方使用下劃線,在不同場(chǎng)合下,有不同含義。本文總結(jié)Python語言編程中常用下劃線的地方,力圖一次搞懂下劃線的常見用法,感興趣的朋友快來一起看看吧2021-05-05Python中用post、get方式提交數(shù)據(jù)的方法示例
最近在學(xué)習(xí)使用Python,發(fā)現(xiàn)網(wǎng)上很少提到如何使用post,所以下面這篇文章主要給大家介紹了關(guān)于Python中用post、get方式提交數(shù)據(jù)的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-09-09