[Python] 使用 pympler 找出參考物件,讓記憶體無法釋放的元兇
昨天 使用 pympler 偵測記憶體洩露 (memory leak),
發現有一些我們的物件持續增長,
因此去程式裡追相關物件被產生的地點、物件被放到哪去、
以及物件裡又包含了什麼東西,
因為 循環參考 (cyclic reference) 很有可能就是這樣產生的。
一個比較簡單的循環參考可能像這樣:
a = A() b = A() a.data = b b.data = a
上例中,a 參考到 b,而 b 也參考到 a,
這就導致 a 和 b 的參考計數 (reference count) 無法降為 0,
因此 a 和 b 都無法被垃圾回收 (garbage collection) 機制清除掉。
當然上述的例子很假,就算這麼寫,應該也很快就能抓出問題。
但實際的程式的循環參考可能是很隱密的,難以肉眼察覺。
這時我們可以利用 pympler.muppy
模組來幫忙,把「某個物件被誰參考到」都列出來,
這樣就更容易定位問題了~
舉例來說,在 使用 pympler 偵測記憶體洩露 (memory leak) 那篇裡面,
我們發現 CurlWrapper 這個型別的物件持續產生,沒被釋放掉。
因此,我讓程式在開始出現記憶體洩露 (memory leak) 後,
去執行下面這段程式:
def mem_obj_referrer(type_name): from pympler import muppy import gc result = {} for obj in muppy.get_objects(): for referent in muppy.get_referents(obj): if type(referent).__name__ == type_name: result.setdefault(referent, []).append(obj) if len(result) >= 100: return result return result from pprint import pprint pprint(mem_obj_referrer("CurlWrapper"))
這段程式裡定義了一個 mem_obj_referrer() 函式,
給它一個型別的名稱,它就會從所有記憶體的物件中,
找出這物件參考到哪些東西 (referent),看看被參考到的是不是我們指定的型別。
這樣就能把參考到我們指定型別物件的「兇手」都找出來。
下面是我執行程式後的部分輸出結果,
可以發現 CurlWrapper 物件被 bound method CurlWrapper.pycurl_callback 參考到:
{<common.curlwrapper.CurlWrapper object at 0x7fd7866cb5d0>: [<bound method CurlWrapper.pycurl_callback of <common.curlwrapper.CurlWrapper object at 0x7fd7866cb5d0>>]}
這個 pycurl_callback() 是 CurlWrapper 物件的一個成員函式,
而 bound method 大概就像下面程式裡的 callback 變數,
它看似一個函式,但綁定了 curl_wrapper 這個物件:
curl_wrapper = CurlWrapper() callback = curl_wrapper.pycurl_callback
單單這樣還不會造成循環參考… 真正的問題在於:
- curl_wrapper 裡面有個 pycurl.Curl 物件
- 我們將 callback 設給 pycurl.Curl 物件,作為回呼函式使用
以上導致 curl_wrapper 參考到 pycurl.Curl 物件,
而 pycurl.Curl 物件參考到 callback 這個 bound method,
連帶參考到它綁定的 curl_wrapper 物件,因而造成了循環參考。
知道原因的話,要解決就比較簡單了,
只要打斷至少其中一條鏈結就可以了,
像是在結束時,設定一個與 curl_wrapper 無關的函式給 pycurl.Curl 使用,
這樣 pycurl.Curl 就不再參考到 curl_wrapper 物件,
自然就可以被垃圾回收機制清掉了~
這個取得參考者的部分,我們用了 muppy.get_referents()
,
再去倒推參考到這個物件的人,感覺不太直覺。
原本有試過 gc.get_referrers()
函式,不過不知為何,
沒能列出參考到物件的人,但 muppy.get_referents()
倒推回去是可以的,
這也還是一個未解的謎囉~
修正程式後,看一下記憶體使用量的增長,
尾端看起來接近平順,應該已經沒有很明顯的記憶體洩露囉: