簡書 佔小狼
轉載請註明原創出處,謝謝

上周有幸給部門的小夥伴分享了一些JVM相關的知識,在整個做PPT的過程中,也是對一個領域的碎片知識的整理,本文將針對虛擬機GC相關的一些內容進行整理,本文不會涉及到G1收集器。

在Hotspot VM實現中,主要有兩大類GC

  1. Partial GC:並不會堆整個GC堆進行收集
    • young gc:只收集 young gen 的GC
    • old gc:只收集 old gen 的GC,只有CMS的 concurrent collection
    • mixed GC:收集整個 young gen 以及部分 old gen 的GC,只有G1
  2. Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等

其實在各種文章或書上還可以看到Minor GC、Major GC的字眼,其中minor GC和young gc對應,而Major GC通常是和Full GC是等價的,由於HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,所以Major GC有時也可能是指old gc,在下定論之前一定要先問清楚。

單線程、并行、併發

在GC收集器實現中,分為了單線程、并行和併發。
單線程收集器:如 Serial GC,這個比較好理解,即垃圾收集過程中只有單一線程在進行收集工作,實現也最簡單。

并行收集器:如Parallel GC,每次運行時,不管是YGC,還是FGC,會 stop-the-world,暫停所有的用戶線程,並採用多個線程同時進行垃圾收集。

併發收集器:如CMS GC,在新生代進行垃圾收集時和并行收集器類似,都是并行收集(當然具體算法中,你也可以設置成採用單線程進行收集),而且都會stop-the-world,主要的區別在於老年代的收集上,CMS在老年代進行垃圾收集時,大部分時間可以和用戶線程併發執行的,只有小部分的時間stop-the-world,這就是它的優勢,可以大大降低應用的暫停時間,當然也是有劣勢的。

算法組合

Hotspot VM實現的幾種GC算法組合中,其中CMS GC使用最廣,因為現在都是大內存時代。

1、Serial GC

Serial generational collector (-XX:+UseSerialGC)
是全局範圍的Full GC,這種算法組合是最早出現的,當年的Java堆內存大小都還不大,使用Serial GC進行單線程收集,還感覺不出來GC耗時導致應用暫停的問題

2、Parallel GC

Parallel for young space, serial for old space generational collector (-XX:+UseParallelGC).
Parallel for young and old space generational collector (-XX:+UseParallelOldGC)
當Java堆慢慢變大時,發現已經無法忍受GC耗時帶來的應用暫停了,出現了Parallel GC,採用多線程的方式進行垃圾收集,很明顯可以提升垃圾收集效率。

3、CMS GC

Concurrent mark sweep with serial young space collector (-XX:+UseConcMarkSweepGC
–XX:-UseParNewGC)
Concurrent mark sweep with parallel young space collector (-XX:+UseConcMarkSweepGC)
當Java堆達到更大時,比如8G,使用Parallel GC帶來的應用暫停已經很明顯了,所有又出現了 CMS GC,這是目前我看到線上環境使用的比較多的GC策略,在參數中添加-XX:+UseConcMarkSweepGC,對於 young gen,會自動選用 ParNewGC,不需要額外添加 -XX:+UseParNewGC

CMS雖然好,因為它的特殊算法,大部分的收集過程可以和用戶線程併發執行,大大降低應用的暫停時間,不過也會帶來負面影響,在收集完 old gen 之後,CMS並不會做整理過程,會產生空間碎片,如果這些碎片空間得不到利用,就會造成空間的浪費,整個過程中可能發生 concurrent mode failure,導致一次真正意義的 full gc,採用單線程對整個堆(young+old+perm) 使用MSC(Mark-Sweep-Compact)進行收集,這個過程意味着很慢很慢很慢,而且這個碎片問題是無法預測的.

4、G1 GC

G1 garbage collector (-XX:+UseG1GC),本文不對G1進行介紹

觸發條件

young gc

對於 young gc,觸發條件似乎要簡單很多,當 eden 區的內存不夠時,就會觸發young gc,我們看看在 eden 區給對象分配一塊內存是怎樣一個過程,畫了一個簡單的流程圖,我一直覺得一個好的示意圖可以讓一個枯燥的過程變得更有意思。


在 eden 區分配空間內存不足時有兩種情況,為對象分配內存、為TLAB分配內存,總之就是內存不夠,需要進行一次 young gc 為eden區騰出空間為後續的內存申請做準備,然後由一個用戶線程通知VM Thread,接下去要執行一次 young gc。

full gc

1、old gen 空間不足

當創建一個大對象、大數組時,eden 區不足以分配這麼大的空間,會嘗試在old gen 中分配,如果這時 old gen 空間也不足時,會觸發 full gc,為了避免上述導致的 full gc,調優時應盡量讓對象在 young gc 時就能夠被回收,還有不要創建過大的對象和數組。

2、統計得到的 young gc 晉陞到 old gen的對象平均總大小大於old gen 的剩餘空間

當準備觸發一次 young gc時,會判斷這次 young gc 是否安全,這裏所謂的安全是當前老年代的剩餘空間可以容納之前 young gc 晉陞對象的平均大小,或者可以容納 young gen 的全部對象,如果結果是不安全的,就不會執行這次 young gc,轉而執行一次 full gc

3、perm gen 空間不足

如果有perm gen的話,當系統中要加載的類、反射的類和調用的方法較多,而且perm gen沒有足夠空間時,也會觸發一次 full gc

4、ygc出現 promotion failure

promotion failure 發生在 young gc 階段,即 cms 的 ParNewGC,當對象的gc年齡達到閾值時,或者 eden 的 to 區放不下時,會把該對象複製到 old gen,如果 old gen 空間不足時,會發生 promotion failure,並接下去觸發full gc

在GC日誌中,有時會看到 concurrent mode failure 關鍵字,這是因為什麼原因導致的問題呢? 對這一塊的理解,很多文章都是說因為 concurrent mode failure 導致觸發full gc,其實應該反過來,是full gc 導致的 concurrent mode failure,在cms gc的算法實現中,通常說的cms是由一個後台線程定時觸發的,默認每2秒檢查一次old gen的內存使用率,當 old gen 的內存使用率達到-XX:CMSInitiatingOccupancyFraction設置的值時,會觸發一次 cms gc,對 old gen 進行併發收集,而真正的 full gc 是通過 vm thread線程觸發的,而且在判斷當前ygc會失敗的情況下觸發full gc,如上一次ygc出現了promotion failure,如果執行 full gc 時,發現後台線程正在執行 cms gc,就會導致 concurrent mode failure。

對於以上這些情況,CMSInitiatingOccupancyFraction參數的設置就顯得尤為重要,設置的太大的話,發生CMS時的剩餘空間太小,在ygc的時候容易發生promotion failure,導致 concurrent mode failure 發生的概率就增大,如果設置太小的話,會導致 cms gc 的頻率會增加,所以需要根據應用的需求對該參數進行調優。

5、執行 System.gc()jmap -histo:live <pid>jmap -dump ...

參考資料
Major GC和Full GC的區別是什麼?觸發條件呢

個人公眾號



分享