來消化之前有讀沒時間寫的東西。
長期使用 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。
不像 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__。