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

java的gc為什麽要分代?

2016-12-14數位

題主對「GC roots」的概念的理解 完全錯誤

如果題主是把「GC roots」(根參照集合)實質上理解成了「live set」(存活的物件的集合)的概念的話,這就好理解了。否則很難理解題主想說的是什麽意思。

=================================================

題主說:

我的理解是jvm不可能把GC roots找全乎了,把所有的gc root全部找出來也可以但是效率太低

所謂「GC roots」,或者說tracing GC的「根集合」,就是 一組必須活躍的參照

例如說,這些參照可能包括:

  • 所有Java執行緒當前活躍的棧幀裏指向GC堆裏的物件的參照;換句話說,當前所有正在被呼叫的方法的參照型別的參數/局部變量/臨時值。
  • VM的一些靜態數據結構裏指向GC堆裏的物件的參照,例如說HotSpot VM裏的Universe裏有很多這樣的參照。
  • JNI handles,包括global handles和local handles
  • (看情況)所有當前被載入的Java類
  • (看情況)Java類的參照型別靜態變量
  • (看情況)Java類的執行時常量池裏的參照型別常量(String或 class型別)
  • (看情況)String常量池(StringTable)裏的參照
  • 註意,是一組必須活躍的 參照 ,不是物件。

    Tracing GC的根本思路就是:給定一個集合的參照作為根出發,透過參照關系遍歷物件圖,能被遍歷到的(可到達的)物件就被判定為存活,其余物件(也就是沒有被遍歷到的)就自然被判定為死亡。註意再註意:tracing GC的本質是透過找出所有活物件來把其余空間認定為「無用」,而不是找出所有死掉的物件並回收它們占用的空間。

    GC roots這組參照是tracing GC的 起點 。要實作語意正確的tracing GC,就必須要能完整列舉出 所有的GC roots ,否則就可能會漏掃描應該存活的物件,導致GC錯誤回收了這些被漏掃的活物件。

    這就像任何遞迴定義的關系一樣,如果只定義了遞推項而不定義初始項的話,關系就無法成立——無從開始;而如果初始項定義漏了內容的話,遞推出去也會漏內容。

    那麽分代式GC對GC roots的定義有什麽影響呢?

    答案是:分代式GC是一種部份收集(partial collection)的做法。在執行部份收集時,從GC堆的非收集部份指向收集部份的參照,也必須作為GC roots的一部份。

    具體到分兩代的分代式GC來說,如果第0代叫做young gen,第1代叫做old gen,那麽如果有minor GC / young GC只收集young gen裏的垃圾,則young gen屬於「收集部份」,而old gen屬於「非收集部份」,那麽從old gen指向young gen的參照就必須作為minor GC / young GC的GC roots的一部份。

    繼續具體到HotSpot VM裏的分兩代式GC來說,除了old gen到young gen的參照之外,有些帶有弱參照語意的結構,例如說記錄所有當前被載入的類的SystemDictionary、記錄字串常量參照的StringTable等,在young GC時必須要作為strong GC roots,而在收集整堆的full GC時則不會被看作strong GC roots。

    換句話說,young GC比full GC的GC roots還要更大一些。如果不能理解這個道理,那整個討論也就無從談起了。

    順帶放幾個傳送門:

  • Major GC和Full GC的區別是什麽?觸發條件呢?- RednaxelaFX 的回答 - 知乎
  • 主流的垃圾回收機制都有哪些? - RednaxelaFX 的回答 - 知乎
  • 關於CMS、G1垃圾回收器的重新標記、最終標記疑惑? - RednaxelaFX 的回答 - 知乎
  • 火車演算法在目前在哪些JVM中有用到,G1嗎? - RednaxelaFX 的回答 - 知乎
  • =================================================

    那麽分代有什麽好處?

    對傳統的、基本的GC實作來說,由於它們在GC的整個工作過程中都要「stop-the-world」,如果能想辦法縮短GC一次工作的時間長度就是件重要的事情。如果說收集整個GC堆耗時太長,那不如只收集其中的一部份?

    於是就有好幾種不同的劃分(partition)GC堆的方式來實作部份收集,而分代式GC就是這其中的一個思路。

    這個思路所基於的基本假設大家都很熟悉了:weak generational hypothesis——大部份物件的生命期很短(die young),而沒有die young的物件則很可能會存活很長時間(live long)。

    這是對過往的很多套用行為分析之後得出的一個假設。基於這個假設,如果讓新建立的物件都在young gen裏建立,然後頻繁收集young gen,則大部份垃圾都能在young GC中被收集掉。由於young gen的大小配置通常只占整個GC堆的較小部份,而且較高的物件死亡率(或者說較低的物件存活率)讓它非常適合使用copying演算法來收集,這樣就不但能降低單次GC的時間長度,還可以提高GC的工作效率。

    放幾個傳送門:

  • JVM GC遍歷一次新生代所有物件是否可達需要多久?- RednaxelaFX 的回答 - 知乎
  • 有關 Copying GC 的疑問?- RednaxelaFX 的回答 - 知乎
  • 但是!有些比較先進的GC演算法是增量式(incremental)的,或者部份並行(mostly-concurrent),或者幹脆完全並行(fully-concurrent)的。

    例如鄙司Azul Systems的Zing JVM裏的C4 GC,就是一個完全並行的GC演算法。它不存在「GC整個工作流程中都要把套用stop-the-world」的問題——從演算法的設計上就不存在。

    然而C4卻也是一個分兩代的分代式GC。為什麽呢?

    C4 GC的前身是Azul System的上一代JVM裏的「Pauseless GC」演算法,而Pauseless是一個完全並行但是不分代的GC。

    Oracle的HotSpot VM裏的G1 GC,在最初設計的時候是不分代的部份並行+增量式GC,而後來在實際投入生產的時候使用的卻也是分兩代的分代式GC設計。

    現在Red Hat正在開發中的Shenandoah GC是一個並行GC,它目前的設計還是不分代的,但根據過往經驗看,它後期漸漸發展為分代式的可能性極其高——如果這個計畫能活足夠久的話。

    對於這些GC來說,解決stop-the-world時間太長的問題並不是選擇分代的主要原因。

    就Azul的Pauless到C4的發展歷程來看,選擇實作分代的最大好處是,GC能夠應付的套用記憶體分配速率(allocation rate)可以得到巨大的提升。

    並行GC根本上要跟套用玩追趕遊戲:套用一邊在分配,GC一邊在收集,如果GC收集的速度能跟得上套用分配的速度,那就一切都很完美;一旦GC開始跟不上了,垃圾就會漸漸堆積起來,最終到可用空間徹底耗盡的時候,套用的分配請求就只能暫時等一等了,等GC追趕上來。

    所以,對於一個並行GC來說,能夠盡快回收出越多空間,就能夠應付越高的套用記憶體分配速率,從而更好地保持GC以完美的並行模式工作。

    雖然並不是所有套用中的物件生命周期都完美吻合weak generational hypothesis的假設,但這個假設在很大範圍內還是適用的,因而也可以幫助並行GC改善效能。

    就先寫這麽多…