Python的記憶體流失及gc模組的使用
-- 6.11日錯誤修正版
Horin|賀勤
Email: horin153@msn.com
Blog:
http://blog.csdn.net/horin153/
在 Python
中,為瞭解決記憶體流失問題,採用了對象引用計數,並基於引用計數實現自動記憶體回收。
因為 Python
有了自動記憶體回收功能,不少初學者就認為自己從此過上了好日子,不必再受記憶體流失的騷擾了。但如果查看一下 Python 文檔對 __del__()
函數的描述,就知道好日子裡也是有陰雲的。下面摘抄一點文檔內容:
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__()
函數的對象間的循環參考是導致記憶體流失的主凶。特別說明:對沒有 __del__() 函數的 Python
對象間的循環參考,是可以被自動記憶體回收掉的。
如何知道一個對象是否記憶體流失了呢?
方法一、當你認為一個對象應該被銷毀時(即引用計數為 0),可以通過 sys.getrefcount(obj)
來擷取對象的引用計數,並根據傳回值是否為 0 來判斷是否記憶體流失。如果返回的引用計數不為 0,說明在此刻對象 obj
是不能被記憶體回收行程回收掉的。
方法二、也可以通過 Python 擴充模組 gc 來查看不能回收的對象的詳細資料。
首先,來看一段正常的測試代碼:
#--------------- 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()
#---------------
code end ----------------
在 test_gcleak() 中,設定記憶體回收行程調試標誌後,再用
collect() 進行記憶體回收,最後列印出該次記憶體回收發現的不可達的垃圾對象數和整個解譯器中的垃圾對象數。
gc.garbage 是一個 list 對象,清單項目是垃圾收集器發現的不可達(即是垃圾對象)、但又不能釋放(即不能回收)的對象。文檔描述為:A
list of objects which the collector found to be unreachable but could
not be freed (uncollectable objects).
通常,gc.garbage
中的對象是引用環中的對象。因為 Python 不知道按照什麼樣的安全次序來調用環中對象的 __del__() 函數,導致對象始終存活在
gc.garbage 中,造成記憶體流失。如果知道一個安全的次序,那麼就打破引用環,再執行 del gc.garbage[:]
,以清空垃圾對象列表。
上段代碼輸出為(#後字串為筆者所加註釋):
#-----------------------------------------
begin
leak test...
# 變數 _gcleak 的引用計數為 2.
_gcleak ref count0:2
#
_gcleak 變為不可達(unreachable)的非法變數.
_gcleak is invalid!
# 開始記憶體回收
begin
collect...
# 本次記憶體回收發現的不可達的垃圾對象數為 0.
unreachable object num:0
#
整個解譯器中的垃圾對象數為 0.
garbage object num:0
#-----------------------------------------
可見 _gcleak 對象的引用計數是正確的,也沒有任何對象發生記憶體流失。
如果不注釋掉
make_circle_ref() 中的 test_code_1 語句:
_gcleak._self = _gcleak
也
就是讓 _gcleak 形成一個自己對自己的循環參考。再運行上述代碼,輸出結果就變成:
#-----------------------------------------
begin
leak test...
_gcleak ref count0:3
_gcleak is invalid!
begin
collect...
# 發現可以回收的垃圾對象: 地址為 012AA090,類型為 CGcLeak.
gc:
uncollectable <CGcLeak 012AA090>
gc: uncollectable <dict
012AC1E0>
unreachable object num:2
#!! 不能回收的垃圾對象數為 1,導致記憶體流失!
garbage
object num:1
#-----------------------------------------
可見
<CGcLeak 012AA090> 對象發生了記憶體流失!!而多出的 dict 垃圾就是泄漏的 _gcleak
對象的字典,列印出字典資訊為:
{'_self': <__main__.CGcLeak object at
0x012AA090>, '_text': '##########'}
除了對自己的循環參考,多個對象間的循環參考也會導致記憶體流失。簡單舉例如下:
#--------------- 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 ----------------
這次測試後輸出結果為:
#-----------------------------------------
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 對象都發生了記憶體流失。因為二者是循環參考,記憶體回收行程不知道該如何回收,也就是不知道該首先調用那個對象的 __del__()
函數。
採用以下任一方法,打破環狀引用,就可以避免記憶體流失:
[1] 注釋掉 make_circle_ref()
中的 test_code_2 語句;
[2] 注釋掉 make_circle_ref() 中的 test_code_3 語句;
[3]
取消對 make_circle_ref() 中的 test_code_4 語句的注釋。
相應輸出結果變為:
#-----------------------------------------
begin
leak test...
ref count0:a=2 b=3 # 註:此處輸出結果視情況變化.
_a is invalid!
_b
is invalid!
begin collect...
unreachable object num:0
garbage
object num:0
#-----------------------------------------
結論:Python 的 gc 有比較強的功能,比如設定 gc.set_debug(gc.DEBUG_LEAK)
就可以進行循環參考導致的記憶體泄露的檢查。如果在開發時進行記憶體泄露檢查;在發布時能夠確保不會記憶體泄露,那麼就可以延長 Python
的記憶體回收時間間隔、甚至主動關閉記憶體回收機制,從而提高運行效率。
推薦閱讀文獻
1,
Python記憶體回收演算法描述,http://wiki.woodpecker.org.cn/moin/python_ref_circle_gc
2,
Garbage Collection for Python,http://arctrix.com/nas/python/gc/