JVM系列之GC

我又不是架構師發表於2017-11-28

JVM系列之GC

談到JVM,大家都知道GC(Garbage Collection),GC這個話題說淺了就一句話--JVM自動垃圾收集,說深了就無止盡了,回收演算法,各種收集器,gc型別,gc觸發點....等等,作者也是略懂皮毛,這裡給大家推薦一個知乎上比較活躍的JVM大牛,RednaxelaFX,是專門做JVM開發的,業界號稱"R大"。放個傳送門:R大
鑑於作者才學疏淺,這篇博文還是準備用通熟易懂的話把作者自己對GC這一塊的理解做陳述,概要如下:

文章結構

  1. 哪些記憶體需要回收(Which)
  2. 各種GC的觸發時機(When)
  3. 如何回收(How)
    3.1 回收演算法
    3.2 HotSpot的具體實現-各種收集器
  4. GC日誌

1. 哪些記憶體需要回收(Which)

大多數沒幹過C或者C++的Javaer是幸福的,因為沒有體會過那種自己new delete記憶體的感覺,建立物件就是new,不管記憶體的回收問題。其實我們的記憶體是JVM的GC機制來幫我們回收的。那麼問題來了。到底哪些記憶體需要回收呢?
答案:可達性分析演算法,說白了,就是JVM預先確定一組GC roots引用變數,如Student stu =new Student();這個stu就可以作為GC roots,當進行垃圾回收時,JVM通過GC Roots找到能夠引用到的所有活物件,然後把剩下的物件標記為"無用",即可回收狀態
能夠作為GC roots的引用如下:

  • 所有Java執行緒當前活躍的棧幀裡指向GC堆裡的物件的引用;換句話說,當前 所有正在被呼叫的方法的引用型別的引數/區域性變數/臨時值。
  • VM的一些靜態資料結構裡指向GC堆裡的物件的引用,例如說HotSpot VM裡的Universe裡有很多這樣的引用。
  • JNI handles,包括global handles和local handles(看情況)所有當前被載入的Java類(看情況)Java類的引用型別靜態變數(看情況)Java類的執行時常量池裡的引用型別常量(String或Class型別)(看情況)String常量池(StringTable)裡的引用

2. 各種GC的觸發時機(When)

2.1 GC型別

說到GC型別,就更有意思了,為什麼呢,因為業界沒有統一的嚴格意義上的界限,也沒有嚴格意義上的GC型別,都是左邊一個教授一套名字,右邊一個作者一套名字。為什麼會有這個情況呢,因為GC型別是和收集器有關的,不同的收集器會有自己獨特的一些收集型別。所以作者在這裡引用R大關於GC型別的介紹,作者覺得還是比較妥當準確的。如下:

  • Partial GC:並不收集整個GC堆的模式
    • Young GC(Minor GC):只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
    • Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
  • Full GC(Major GC):收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

2.2 觸發時機

上面大家也看到了,GC型別分分類是和收集器有關的,那麼當然了,對於不同的收集器,GC觸發時機也是不一樣的,作者就針對預設的serial GC來說:

  • young GC:當young gen中的eden區分配滿的時候觸發。注意young GC中有部分存活物件會晉升到old gen,所以young GC後old gen的佔用量通常會有所升高。
  • full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。

3. 如何回收(How)

3.1 回收演算法

由於網上已經擁有非常多的優秀博文來詳細介紹關於回收演算法這塊,所以這塊作者將引用其他部落格的介紹並加上自己的一些描述:
3.1.1 標記清除演算法(Mark-Sweep)


3.1.2複製演算法(Coping)(絕大部分收集器的新生代使用的演算法)

複製演算法在JVM新生代垃圾回收中的運用:

Eden:From:TO =8:1:1
由於新生代中90%的物件都是"朝生夕死",採用複製演算法是比較合理的,首先只移動了存活下來的物件(比較少數),其次,記憶體在移動到To區域後是有順序的,不存在記憶體碎片。
值得一提的是,假如在一次MinorGC時,Eden中存活的物件+From中存活的物件>To的剩餘空間,則會通過擔保機制將物件直接轉移到Old gen ,如果Old gen的記憶體空間也不夠,則進行一次Full gc .
當物件的年齡到達15歲時會轉移到Old gen (可通過引數配置,一般不建議更改。)

3.1.3標記-整理演算法(Mark-Compact):

由於Old gen 的大部分物件都是年齡很大的物件,所以存活率比較高,採用複製演算法肯定是行不通的(較多的物件複製操作),所以才大部分收集器的old gen採用 Mark-Compact演算法,避免了空間碎片。

3.1.4三種演算法比較:

稍微解釋一下常見的關於GC時間的問題:
為什麼FGC的時間比MinorGC長很多?
答:FGC進行了old gen的gc,由於演算法上採用Mark-Sweep或者Mark-Compact,進行了很多物件(老年代存活率很低)的移動,當然很耗時了!其實就是空間換時間,時間換空間的問題。

3.2 HotSpot的具體實現-各種收集器

關於收集器這塊,由於本人也是JVM初學者,加上很少有在生產環境做收集器引數調整,搭配使用的機會。所以可以說對於一些HotSpot收集器只是停留在
書籍與博文層次,所以這裡就不賣弄了。下面給一個傳送門大家自行看一看吧:
www.jianshu.com/p/50d5c88b2…

4 GC日誌

-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-Xloggc:/Users/zdy/Desktop/dump/gclog.txt

當伺服器出現卡頓比較頻繁時,嘗試看下自己的GC日誌,注意Full gc 頻率。

最後,稍微說一下作者的心得:

  • 如果是伺服器一次卡頓時間比較長,一般是full gc時間過長,而應用最求的是卡頓(STW)時間短,可以接受多次卡頓,那麼可以考慮調整加大young gen的比例,和提高進入老年代的年齡(預設15歲)來儘量避免FGC.
  • 選擇合適的收集器很重要。要根據應用的特點。是追求吞吐量,還是追求最小停頓。
  • 經常對照gc日誌觀察現實的情況,如多長時間一卡頓,多久一卡頓,然後來調整自己的收集器或者相關的記憶體比例來達到自己想要的效果。
  • 在有限的物理資源條件下,要避免讓使用者接受過多的STW,可以考慮在半夜自動進行gc(System.gc()),雖然不一定生效,但可以觀察下效果。多數情況下是會觸發full gc 的。
  • 大多數應用是可以接受頻繁的mgc,但卻不能接受full gc 的長時間卡頓,所以在觀察gc日誌時一定要注意自己full gc的頻率和觸發條件(是由於記憶體擔保,還是年齡到了,還是TO記憶體太小,導致每次都fgc.).
    由於作者在gc這一塊也不是特別厲害,並且缺少一定的實戰經驗,不敢妄自菲薄,所以給傳送門供大家參考閱讀:
    www.jianshu.com/p/088d71f20…
    www.cnblogs.com/mikevictor0…
    www.cnblogs.com/mikevictor0…

相關文章