CPython 的 garbage collection

來消化之前有讀沒時間寫的東西。

長期使用 python 後, 會很納悶為啥記憶體一直漲, 明明已沒用到了, 卻沒有減少。這篇提到 CPython 的 gc 機制採用 reference counting, 另有備案的 mark and sweep, 用來解決 circular reference。btw, 強烈推薦 Back to basic: Series on dynamic memory management, 淺顯地介紹各種 gc 運作的方式。

Scott 的說法, reference counting 算是苦工的半自動化管理記憶體, 因為實作細節交給實作者處理, 用 Python API 寫 CPython 的 extension 時, 開發者得自己管理物件的 reference count, 是件很辛苦的事。好處是在 reference count 為 0 的時候, 會立即回收記憶體。但缺點是, 若 a、b 兩物件互相參照到對方, 卻沒被任何人用到的話, a 和 b 都無法被回收, 因為 reference count 永遠會是 1。

為解決這個問題, 只好多提供 gc.collect 喚起 mark and sweep 的演算法清掉 a、b。但 mark and sweep 也有自己的問題, 主要有兩點:

  • 不像 reference counting 會在沒用到的第一時間點立即回收, 而是 mark and sweep 執行後才回收。
  • 執行 mark and sweep 的時候為確保沒有算錯 reference graph, 必須暫停目前所有執行中的 python process/thread, 結束後才可繼續執行。可想而知, 記憶體用量大後, 這個暫停時間也會太久, 不適合需要不斷有回應的程式 (如 GUI、Web)。

所以又有了 generational garbage collection, 關鍵的想法是: 觀察到大部份物件都很早死 (英年早逝啊~), 所以只要回收年輕的物件即可回收大部份的記憶體。於是將使用到的物件分不同「年代」, 預設只回收最年輕的一代, 沒回收到的物件就放到下一代。下回要再回收的時候, 就不動這個舊一代的物件, 減少要建立的 reference graph。

回想平時寫程式的結構, 可以想見, 像頻繁使用的 local variable 仍會被這個方法回收, 偶而用到的 global variable 只有第一次會算到, 之後被當作舊一代的物件後, 就不會再檢查。這樣只要有個方式確保不會漏查舊一代指向新一代的物件, 就可確保不會誤刪仍用到的物件。至於明明可回收卻沒回收到的物件, 相較於省下的時間來說, 算是可接受的取捨。generational garbage collection 值得一看, 有提到一些巧思, 取得計算時間和空間的平衡。

可想而知, CPython 用的是 generational gc, 而 JVM 也是。看來好東西大家都會一起用。

Btw, 即使了解了這些仍無法解釋為啥 CPython 有一堆沒有歸還一堆沒在用的記憶體, 實際的情況比想像中還複雜, CPython 有一些「絕對不會歸還」的記憶體, 像是 small integer pool, 和重覆回收使用的 PyIntObject, 或是有 circular reference 且這之中有物件實作 __del__。

2011-12-04 Update

看到 Thinker 針對本篇的補充, 貼過來備忘: CPython 的 GC 二、三事

留言

  1. 記憶體用量高漲不下有很多原因,你可以用guppy去看到底是哪些物件還卡在那裡,我個人有遇過物件本身的佔量很少,但整個process的RSS高居不下,推斷是記憶體碎片,因為小於255的區塊會用它的memory pool存放,超過就會用malloc申請,當一直重覆申請釋放,就會造成memory中的不可用的小氣泡,那些page可能會較難swap out,造成RSS高居不下,可以參考我寫的這篇

    http://blog.ez2learn.com/2010/04/22/memory-efficient-python-with-bytearray/

    回覆刪除
  2. 謝啦, 之後又有類似需求時來試試

    回覆刪除

張貼留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

熟悉系統工具好處多多

virtualbox 使用 USB 裝置