當前位置: 華文問答 > 數位

Java 等語言的 GC 為什麽不即時釋放記憶體?

2013-11-01數位

樓主這問題跟之前另一個問題相關:

垃圾回收機制中,參照計數法是如何維護所有物件參照的?

建議先讀讀那個問題的解答再看下面。

關鍵點在:

  1. 最基本的純參照計數方式的自動記憶體管理可以做到即時釋放死物件,但卻無法處理存在迴圈參照的物件圖的釋放。這個問題一定程度上可以透過引入弱參照的概念來解決,但通用的能處理帶迴圈參照物件圖的參照計數都是有別的管理方式備份的(通常是某種tracing GC,例如mark-sweep;也有名為「trial-deletion」的迴圈檢測方法,但這個通常比tracing效能更差所以用得較少),例如CPython使用以參照計數為主、mark-sweep為輔的方式,Adobe Flash的ActionScript VM 2(AVM2)也是以延遲參照計數(DRC)為主、增量/保守式mark-sweep為輔。反之,像C++的std::shared_ptr就是純參照計數,無法靠自己處理帶迴圈參照的物件圖,而必須靠程式設計師自己小心使用,在必要的地方用std::weak_ptr來破除迴圈;CPython在2.0之前也使用純參照計數,無法處理迴圈參照,只能等著泄漏記憶體。既然通用的參照計數還得用tracing GC來備份,實作這樣的自動記憶體管理等於得實作兩份,想偷懶的話還不如一開始就只實作某種tracing GC,例如mark-sweep。
  2. 最基本的純參照計數方式對參照計數器的操作非常頻繁,這裏有額外開銷,至於是否嚴重到成問題就看具體套用的可忍受程度。 在記憶體充裕的前提下 ,基本的tracing GC比基本的參照計數方式的效能更好(特別是從throughput角度看),不需要做冗余的計數器更新。同時,在多執行緒環境下參照計數器可能成為執行緒間共享的數據,需要做同步保護(這裏把原子更新算同步保護的一種),這也是個額外開銷的來源;因為tracing GC不需要維護參照計數器所以也就沒有這種同步的開銷。參照計數的這些效能缺點可以透過一些高級變種來緩解,例如前面提到AVM2的延遲參照計數,只記錄堆上物件之間的參照計數而不記錄棧上(主要是運算式臨時值)對物件的參照計數,以此減少對計數器的更新次數來提高效能。詳情可參考文件:MMgc | MDN。這些參照計數的高級變種通常意味著一定程度的延遲釋放,跟樓主想即時釋放的初衷就不符了。另一方面,雖然最基本的tracing GC會有較長的延遲,但它們也有高級變種,可以並列、並行、增量式執行,降低延遲;也有辦法實作thread-local GC來應對像是「請求-響應」式的Web套用批次釋放一個執行緒臨分時配的物件的需求。
  3. 如果選用tracing GC來實作自動記憶體管理,它是不顯式維護物件的參照計數的,也就沒有「參照計數到0」的概念。所以基於tracing GC的JVM或其它語言的執行時環境自然不會「參照計數到0就釋放物件」。
  4. 參照計數方式其實也有經典的卡頓情況。例子之一就是一個物件個數很多、參照鏈很長的物件圖假如只是被一個參照而留活,那麽那個參照一死就會引發大量物件紮堆釋放(但卻不是「批次釋放」,開銷不同),這一樣會引起卡頓。單純討論最壞情況的話其實參照計數也有這樣糟糕的一面。純人工的malloc()/free()或new/delete可以讓程式設計師人肉找出生命周期相同的物件,然後利用諸如arena之類的方式為它們分配記憶體,就可以它們死的時候真正批次釋放掉它們,這樣就很高效;但純參照計數卻不是這麽回事。使用參照計數會否遇到這種卡頓全看你的程式裏物件圖的參照關系是怎樣的。