(七)JVM成神路之GC分代篇:分代GC器、CMS收集器及YoungGC、FullGC日志剖析

引言

《GC基礎(chǔ)篇》中曾談到過分代以及分區(qū)回收的概念,但基礎(chǔ)篇更多的是建立在GC的一些算法理論上進(jìn)行高談闊論,而本篇則重點(diǎn)會對于分代收集器的實(shí)現(xiàn)進(jìn)行全面詳解,其中會涵蓋串行收集器、并行收集器、三色標(biāo)記、SATB算法、GC執(zhí)行過程、并發(fā)標(biāo)記、CMS收集器等知識,本篇則偏重于分析GC機(jī)制的落地實(shí)現(xiàn),也就是垃圾收集器(Garbage Collector)。

一、堆空間回顧與GC收集器概述

GC覆蓋的范圍有堆空間與元空間,而主要的作用范圍則是堆空間,所以先簡單回顧堆空間后,再對于GC中的一些概念進(jìn)行闡述,有了這些基礎(chǔ)后再對GC收集器進(jìn)行闡述。

1.1、堆空間回顧

在前面《JVM運(yùn)行時內(nèi)存區(qū)域劃分》中曾提及過:JVM的堆空間結(jié)構(gòu)會根據(jù)運(yùn)行時具體采用的GC收集器來決定。在所有的GC收集器中,大體會將堆空間分為分代、分區(qū)兩大類:

堆空間結(jié)構(gòu)

如上圖,分代堆空間中會分為新生代與年老代兩個區(qū)域,而新生代又會分為Eden*1、Survivor*2三塊。其中新生代采用復(fù)制算法,HotSpot中因為調(diào)整了EdenSurvivor區(qū)域的比例為8:1:1,所以說新生代的內(nèi)存最多浪費(fèi)10%,最大容量為80%+10%=90%。而當(dāng)Survivor空間不足以存放存活對象時,會依賴于年老代進(jìn)行分配擔(dān)保,承接符合標(biāo)準(zhǔn)的對象進(jìn)入年老代空間。

1.2、GC收集器概述

上篇的垃圾收集相關(guān)算法是GC機(jī)制的方法論,而垃圾收集器則是GC機(jī)制的具體實(shí)現(xiàn)。

但在Java的生態(tài)中,存在很多款GC收集器,其中并不存在一款最好最優(yōu)的收集器,也不存在所謂的萬能收集器。因為實(shí)際開發(fā)過程中,我們需要根據(jù)項目的業(yè)務(wù)類型,選出對應(yīng)用程序而言最合適的收集器即可。

不過在了解GC收集器之前,首先得明白幾個GC收集器中常見的名詞。

1.2.1、GC收集器中的名詞解釋

在GC收集器中存在一些經(jīng)常出現(xiàn)的名詞,這些名詞也是在認(rèn)識GC收集器之前不得不了解的,如:串行回收、并行回收、獨(dú)占執(zhí)行、并發(fā)執(zhí)行、吞吐量、停頓時間、吞吐量優(yōu)先、響應(yīng)時間優(yōu)先等。

串行、并行與獨(dú)占、并發(fā)
  • ①串行Serial收集:所有用戶線程停止,單條GC線程回收堆的情況被稱為串行回收。
  • ②并行Parallel收集:所有用戶線程停止,多條GC線程回收堆的情況(需多核CPU支持)。
  • ③獨(dú)占Monopoly執(zhí)行:這里是指GC工作時,GC線程會搶占所有資源執(zhí)行,整個應(yīng)用程序會被停止。
  • ④并發(fā)Concurrent執(zhí)行:這里的并發(fā)是指用戶線程和GC線程同時(交替)執(zhí)行的情況,不會停下某類線程。
吞吐量

吞吐量是性能優(yōu)化中的一個重要指標(biāo),它是指CPU用于執(zhí)行用戶代碼的時間與CPU總耗時的比值,在Java中,吞吐量的計算公式為:

吞吐量 = 用戶代碼執(zhí)行總時長 /(用戶代碼執(zhí)行總時長 + 垃圾回收總時長)。

如JVM在線上執(zhí)行了100min,其中執(zhí)行用戶代碼花費(fèi)了99min,垃圾回收總用時1min,那么吞吐量則為99min/(99min+1min)=99%。

停頓時間

停頓時間是指GC收集器在工作時,所有用戶線程(整個應(yīng)用程序)的暫停時間。對于獨(dú)占類的GC收集器而言,停頓時間會比較長。而對于并發(fā)類的GC收集器來說,因為GC線程和用戶線程是交替執(zhí)行的,所以程序的停頓時間會縮短,但總體GC效率不如獨(dú)占GC收集器,因此系統(tǒng)的吞吐量會降低。

基于獨(dú)占收集器和并發(fā)收集器的特性而言,就牽扯出了兩個調(diào)優(yōu)時的新名詞:吞吐量優(yōu)先與響應(yīng)時間優(yōu)先。 相對而言,在設(shè)計系統(tǒng)架構(gòu)選擇GC收集器或進(jìn)行調(diào)優(yōu)時,最終都是在追求更高的吞吐量以及更短的響應(yīng)時間。

  • 吞吐量優(yōu)先:為了確保程序的更高吞吐,允許GC發(fā)生時出現(xiàn)長時間暫停。
  • 響應(yīng)時間優(yōu)先:為了確保用戶更好的體驗,可以犧牲一定的吞吐量換取更快的響應(yīng)速度,發(fā)生GC時暫停時間越短越好。

1.2.2、Java中的GC收集器概述

在如今的官方JDK中,JVM的GC收集器具體實(shí)現(xiàn)存在十款,分別為Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old、G1、ZGC、Shenandoah、Epsilon等,如下:

Java十款GC收集器

在上圖中共有十款GC收集器,它們可以根據(jù)回收時的屬性分為分代和分區(qū)兩種類型:

  • 分代收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old
  • 分區(qū)收集器:G1、ZGC、Shenandoah

其中Epsilon是個例外,這款收集器是JDK11提供的,這款GC收集器俗稱為“廢物收集器”,裝載該收集器的Java程序,在運(yùn)行期間不會發(fā)生任何GC相關(guān)的操作,程序所分配的堆空間一旦用完,Java程序就會因OOM原因退出。Epsilon收集器主要是用于程序上線前做測試使用,如:性能測試、內(nèi)存壓力測試、VM接口測試等。在程序啟動時選擇裝載Epsilon收集器,這樣可以幫助我們過濾掉GC機(jī)制引起的性能假象。

而本篇重點(diǎn)是敘述分代GC,所以重點(diǎn)先分析一下分代收集器。六款分代收集器,它們分別作用于不同的區(qū)域:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 年老代收集器:CMS、Serial Old(MSC)、Parallel Old

分代GC收集器

如上圖所示,兩者之間存在連線則代表兩個GC收集器可以搭配使用,所以一共存在六種搭配方案:

新生代 年老代
Serial CMS(主用)/Serial Old(備用)
Serial Serial Old(MSC)
ParNew CMS(主用)/Serial Old(備用)
ParNew Serial Old(MSC)
Parallel Scavenge Serial Old(MSC)
Parallel Scavenge Parallel Old

在上表中,可以看到CMS是可以和MSC搭配的,關(guān)于具體為何我們后續(xù)分析,也包括為什么Parallel Scavenge不能和CMS進(jìn)行搭配,后續(xù)分析完GC收集器實(shí)現(xiàn)后再闡述。

二、分代GC收集器詳解

JVM中的分代GC收集器,除開被劃分為新生代和年老代外,也會根據(jù)其收集過程,分為單線程和多線程屬性的收集器。其中Serial、Serial Old(MSC)屬于單線程的收集器,而ParNew、Parallel Scavenge、CMS、Parallel Old則屬于并發(fā)型的多線程收集器。但接下來我們會從分代角度出發(fā),對GC收集器進(jìn)行全面闡述。

2.1、新生代GC收集器詳解

前面提到過新生代收集器主要包含Serial、ParNew、Parallel Scavenge,首先來看看作用于新生代的Serial收集器。

2.1.1、Serial收集器(單線程)

Serial是最原始的新生代收集器,同時它屬于單線程的GC收集器,所以也被稱為串行收集器。顧名思義,它在執(zhí)行GC工作時,是以單線程運(yùn)行的,并且該收集器在發(fā)生GC時,會產(chǎn)生STW,也就是會停止所有用戶線程。但正由于會停止其他用戶線程,所以在執(zhí)行GC時并不會出現(xiàn)線程間的切換。因此,在單顆CPU的機(jī)器上,它的清理效率非常高。一般來說,采用Client模式運(yùn)行的JVM,選取該款收集器作為內(nèi)嵌GC是個不錯的選擇。

Serial收集器小結(jié):
啟動參數(shù):-XX:+UseSerialGC(開啟該參數(shù)后,年老代會使用MSC)。
收集動作:串行GC,單線程。
采用算法:復(fù)制算法。
STW:GC過程在STW中執(zhí)行。
GC發(fā)生時,執(zhí)行過程如下:

Serial收集器執(zhí)行過程

因為該款收集器GC過程中是需要全程發(fā)生在STW中的,所以基于系統(tǒng)層面來說,對用戶體驗感欠佳。就好比你在線看片(指電影),看兩分鐘轉(zhuǎn)幾圈,看一段時間后又看圈,反反復(fù)復(fù)的卡頓....,對于你而言,這顯然一件令人難以接受的事情。

2.1.2、ParNew收集器(多線程)

ParNew收集器是基于Serial收集器的演進(jìn)版,從嚴(yán)格意義上來看,它可以被稱為Serial收集器的多線程版本,同樣是作用于新生代區(qū)域的收集器。在整個實(shí)現(xiàn)上,除開GC收集階段會使用多條線程回收外,其他實(shí)現(xiàn)幾乎與Serial收集器大致相同。

ParNew收集器小結(jié):
啟動參數(shù):-XX:+UseParNewGC
收集動作:并行GC,多線程。
采用算法:復(fù)制算法。
STW:GC過程發(fā)生在STW中,采用多線程回收。
GC發(fā)生時,執(zhí)行過程如下:

ParNew收集器執(zhí)行過程

因為該款收集器與Serial唯一的不同點(diǎn)就在于使用了多線程,所以GC發(fā)生時仍舊會造成程序停頓。但也因為使用了多線程回收,因此能夠在很大程度上縮短系統(tǒng)的停頓時間,從而能夠帶來比Serial更好的用戶體驗。

但該款GC收集器因為采用了多線程,所以需要多核CPU的支持,該收集器會根據(jù)CPU核數(shù),開啟不同的GC線程數(shù),從而達(dá)到最優(yōu)的垃圾回收效果(也可以通過-XX:ParallelGCThreads參數(shù)指定)。但如若是單核的機(jī)器上運(yùn)行時,其效率可能還不如Serial。

一般如果你的程序是以Server模式運(yùn)行的程序,而老年代又采用了CMS收集器,那么新生代搭配ParNew是個不錯的選擇。

2.1.3、Parallel Scavenge收集器(多線程)

Parallel Scavenge同樣是一款作用于新生代的多線程GC收集器,但與ParNew收集器不同的是:ParNew通過控制GC線程數(shù)量來縮短程序暫停時間,更關(guān)心程序的響應(yīng)時間,而Parallel Scavenge更關(guān)心的是程序運(yùn)行的吞吐量,也就是更注重一段時間內(nèi),用戶代碼執(zhí)行時長與程序執(zhí)行總時長的占比。

Parallel Scavenge收集器小結(jié):
啟動參數(shù):-XX:+UseParallelGC。
收集動作:并行GC,多線程。
采用算法:復(fù)制算法。
STW:GC過程發(fā)生在STW中,采用多線程回收。
GC發(fā)生時,執(zhí)行過程如下:

Parallel Scavenge收集器執(zhí)行過程

從上述小結(jié)來看,PS收集器和ParNew收集器好像并未有太大的區(qū)別。但實(shí)際上它們兩者之間基于的底層GC框架完全不同,同時關(guān)注的方向也完全不同。PS收集器的目標(biāo)是讓程序達(dá)到一個可控制的吞吐量(Throughput),所以PS也被稱為吞吐量優(yōu)先的垃圾收集器。

PS收集器可以通過-XX:MaxGCPauseMillis-XX:GCTimeRatio參數(shù)精準(zhǔn)控制GC發(fā)生時的時間以及吞吐量占比。同時與ParNew收集器最大的不同在于:PS收集器還可以通過開啟-XX:+UseAdaptiveSizePolicy參數(shù),讓JVM啟動自適應(yīng)的GC調(diào)節(jié)策略,開啟該參數(shù)后,JVM會根據(jù)當(dāng)前系統(tǒng)的運(yùn)行狀態(tài)調(diào)整吞吐比與GC時間,從而確保能夠提供最合適的停頓時間和吞吐量。

  • 那如果使用PS收集器的時候,我們通過參數(shù)手動將GC時間設(shè)的很小,然后將吞吐占比設(shè)的很高,豈不是GC回收會變得非常完美?
  • 答案是:并非如此。因為在追求響應(yīng)時間的時候必然會犧牲吞吐量,而追求吞吐量的同時必然會犧牲響應(yīng)時間。好比你通過參數(shù)將GC時間設(shè)置的很小,那么PS在運(yùn)行時會將新生代空間調(diào)小,如從原本的1GB調(diào)整到800MB,收集800MB的空間必然速度會比1GB的快很多。但與之相對應(yīng)的收集頻率會增高,可能原本原來60s收集一次,每次收集停頓100ms,而現(xiàn)如今內(nèi)存被調(diào)小后,40s就要發(fā)生一次GC,每次GC停頓80ms,你可以對比這兩者之間的區(qū)別:
  • 24min/1GB空間-GC開銷:(24min/60s)*100ms=24000ms
  • 24min/800MB空間-GC開銷:(24min/40s)*80ms=28800ms
  • 因此,最終可以得到一個結(jié)果,雖然響應(yīng)時間確實(shí)降低了,但吞吐量也降了下來了。

所以一般線上情況,對于調(diào)優(yōu)沒有豐富經(jīng)驗的情況下,我們不應(yīng)該自己去手動調(diào)整這些參數(shù),而是開啟JVM的自適應(yīng)策略,由JVM自行調(diào)整。

2.2、年老代GC收集器詳解

年老代收集器主要有CMS、Serial Old(MSC)、Parallel Old三款,與新生代的收集器一樣,同樣存在單線程和多線程收集器之分,接下來我們對年老代收集器進(jìn)行依次分析。

2.2.1、Serial Old(MSC)收集器(單線程)

Serial Old(MSC)Serial收集器相同,同樣是一款單線程串行回收的收集器,但不同的是:MSC是一款作用于年老代空間的收集器,它采用標(biāo)記-整理算法對年老代空間進(jìn)行回收。同時,該款收集器也可作為CMS的備用收集器使用。

Serial Old(MSC)收集器小結(jié):
啟動參數(shù):-XX:+UseSerialGC(開啟該參數(shù)后,新生代會使用Serial)。
收集動作:串行GC,單線程。
采用算法:標(biāo)記-整理算法。
STW:GC過程發(fā)生在STW中,采用單線程執(zhí)行串行回收。
GC發(fā)生時,執(zhí)行過程如下:

Serial Old(MSC)收集器執(zhí)行過程

Serial Old(MSC)與新生代收集器Serial差距不大,回收過程也是采用單線程做串行收集,屬于Serial的年老代版本。

2.2.2、Parallel Old收集器(多線程)

Parallel Old則是Parallel Scavenge收集器的年老代版本,同樣采用多線程進(jìn)行并行收集,其內(nèi)部采用標(biāo)記-整理算法。與新生代的PS收集器相同的是:PO同樣追求的是吞吐量優(yōu)先

Parallel Old收集器小結(jié):
啟動參數(shù):-XX:+UseParallelOldGC。
收集動作:并行GC,多線程。
采用算法:標(biāo)記-整理算法。
STW:GC過程發(fā)生在STW中,采用多線程回收。
GC發(fā)生時,執(zhí)行過程如下:

Parallel Old收集器執(zhí)行過程

PO作為PS收集器的年老代版本,其特性與PS大致相同,所以該款收集器同樣適用于注重吞吐量或?qū)PU資源敏感的系統(tǒng)。

2.2.3、CMS收集器(多線程/并發(fā))

CMS收集器全稱為ConcurrentMarkSweep,該款回收器是GC機(jī)制中的一座里程碑,在該款收集器中首次實(shí)現(xiàn)了并發(fā)收集的概念,也就是不停止用戶線程,GC線程與用戶線程一同工作的情況。同時該款收集器追求的是最短的回收時間,屬于多線程收集器,其內(nèi)部采用標(biāo)記-清除算法。

CMS收集器小結(jié):
啟動參數(shù):-XX:+UseConcMarkSweepGC
收集動作:并發(fā)GC,多線程并行執(zhí)行。
采用算法:標(biāo)記-清除算法。
STW:GC過程會發(fā)生STW,但并非整個GC過程都在STW中執(zhí)行,采用多線程回收。
GC發(fā)生時,執(zhí)行過程如下:

CMS收集器執(zhí)行過程

從上面的CMS執(zhí)行圖中可以明確看出,CMS對比其他的GC收集器,回收過程明顯復(fù)雜很多,CMS收集器的回收工作會分為四個步驟:初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記以及并發(fā)清除。

  • ①初始標(biāo)記:僅標(biāo)記GcRoot節(jié)點(diǎn)直接關(guān)聯(lián)的對象,該階段速度會很快,需在STW中進(jìn)行。
  • ②并發(fā)標(biāo)記:該階段主要是做GC溯源工作(GcTracing),從根節(jié)點(diǎn)出發(fā),對整個堆空間進(jìn)行可達(dá)性分析,找出所有存活對象,該階段的GC線程會與用戶線程同時執(zhí)行。
  • ③重新標(biāo)記:這個階段主要是為了修正“并發(fā)標(biāo)記”階段由于用戶線程執(zhí)行造成的GC標(biāo)記變動的那部分對象,該階段需要在STW中執(zhí)行,并且該階段的停頓時間會比初始階段要長不少。
  • ④并發(fā)清除:在該階段主要是對存活對象之外的垃圾對象進(jìn)行清除,該階段不需要停止用戶線程,是并發(fā)執(zhí)行的。
  • PS:其實(shí)在并發(fā)標(biāo)記和重新標(biāo)記中間存在兩步細(xì)節(jié)操作:預(yù)清理以及可終止的預(yù)清理。

在整個收集過程中,除開初始標(biāo)記與重新標(biāo)記階段,其他的收集動作都是與用戶線程并發(fā)執(zhí)行的。因此,CMS收集器在發(fā)生GC時,造成的程序暫停是非常短暫的,對于用戶體驗感而言,相對比之前的收集器而言是最優(yōu)者。也正由于CMS收集器并發(fā)收集、停頓延遲低的特性,所以在有些地方也被稱為并發(fā)低停頓收集器。

從如上的總結(jié)看來,CMS好像很不錯哎~,但實(shí)際上,CMS也存在幾個致命的缺點(diǎn):會產(chǎn)生且無法回收浮動垃圾、對CPU資源非常依賴、GC完成后會造成大量內(nèi)存碎片。

  • ①CMS是一款完全基于多線程環(huán)境研發(fā)的收集器,默認(rèn)情況下,回收過程中開啟的線程數(shù)為(CPU核數(shù)+3)/4,也就代表著:一臺八核的機(jī)器至少要開啟2~3條GC線程。而當(dāng)CPU核數(shù)少于4時,CMS的GC線程則會對用戶線程性能造成很大影響,因為需要讓出一半的CPU運(yùn)算資源去執(zhí)行GC回收工作。
  • ②由于CMS收集器的回收工作是并發(fā)清除垃圾對象的,因此,在清除階段用戶線程依舊在執(zhí)行,而用戶線程執(zhí)行就必然會造成新的垃圾產(chǎn)生,但這部分新產(chǎn)生的垃圾對象是無法標(biāo)記的,所以只能等到下次GC發(fā)生時才可回收,而這部分垃圾則被稱為“浮動垃圾”。
  • ③因為CMS采用的是標(biāo)記-清除算法,所以在回收工作結(jié)束之后會造成大量的內(nèi)存碎片。
    • 為何不采用標(biāo)-整算法呢?因為CMS是并發(fā)執(zhí)行的,所以如果將存活對象壓縮到內(nèi)存一端,那么用戶線程中的所有對象引用都需改變,實(shí)現(xiàn)起來及其復(fù)雜且影響效率。

因為CMS在回收時會產(chǎn)生浮動垃圾以及內(nèi)存碎片,所以CMS一般來說都必須要要搭配一款其他的收集器作為后備方案,而可選項有且只有一個:那就是Serial Old(MSC),當(dāng)內(nèi)存太過碎片化導(dǎo)致無法分配新對象時,或回收一次后存活對象+浮動垃圾占比達(dá)到指定閾值時則會觸發(fā)Serial Old(MSC)收集器回收。
決定著是否觸發(fā)Serial Old(MSC)的關(guān)鍵參數(shù)有三個:

  • -XX:CMSInitIatingOccupancyFaction:需要指定一個百分比,當(dāng)存活對象+浮動垃圾占比達(dá)到該值時會觸發(fā)MSC工作。
  • XX:UseCMSCompactAtFullCollection:該參數(shù)默認(rèn)開啟,當(dāng)內(nèi)存太過碎片化導(dǎo)致無法分配新對象時,觸發(fā)MSC發(fā)生FullGC。
  • XX:CMSFullGCsBeforeCompaction:該參數(shù)可以設(shè)置間隔多少次FullGC后發(fā)生一次整理內(nèi)存碎片的FullGCMSC的GC),默認(rèn)為0,既每次FullGC都會觸發(fā)MSC回收。

2.3、分代GC收集器總結(jié)

就目前而言,分析過的GC收集器中,根據(jù)分代特征,可分為新生代、年老代收集器?;诰€程角度出發(fā),則可分為單線程串行、多線程并行收集器。而從關(guān)注度來看,又可分為吞吐量優(yōu)先、響應(yīng)時間優(yōu)先兩大類。

一般而言,如果你的程序是更為關(guān)注用戶體驗度,那么可以采用響應(yīng)速度優(yōu)先的收集器工作,因為該類收集器造成的程序暫停不會很久。但如若你的程序不需要與用戶有特別多的交互,如批量處理、訂單處理、報表計算、科學(xué)計算等類型的后臺系統(tǒng),那你則可以采用吞吐量優(yōu)先的收集器,因為高吞吐量可以高效率地利用CPU資源。

三、收集器組合方案、CMS三色標(biāo)記與跨代引用

3.1、GC組合方案分析

在第二個段落中,我們詳細(xì)分析了JVM中每款不同的GC收集器,但在實(shí)際開發(fā)過程中,我們的程序采用哪個組合更好呢?其實(shí)并不存在所謂的最好組合,你要選擇那套組合作為Java程序的收集器,更多的需根據(jù)具體的業(yè)務(wù)場景來決定。

如果你的程序追求低延遲,用戶交互度較為頻繁,那你可以采用ParNew + CMS組合(這也是淘寶早期的選擇,但后面采用了自研JVM)。

如若你的程序追求高吞吐,后臺計算工作較多,那么Parallel Scavenge + Parallel Old這組PS+PO的收集器會更適合你。

但你的程序?qū)懗鰜砗?,更多的情況下部署在單核或雙核的機(jī)器時,那么最經(jīng)典的Serial + Serial Old組合絕對是你的最佳選擇。

Java中的分代收集器

我們再一次將目光聚集在這張圖上,需要值得注意的是:在JDK1.8之前,可以采用虛線組合,但在JDK1.8之后,取消了上圖中紅線的組合,被視為棄用的收集器組合(但如果要用,也是可以用的)。到了JDK1.9時,紅線組合被移除,也就代表著在1.9中無法再指定紅線組合作為收集器使用。而到了后面的JDK14時,綠線組合也被棄用,同時官方也移除了CMS收集器,為了給G1鋪路,使用G1代替了CMS。

3.1.1、為何PS收集器不能和CMS收集器搭配使用?

因為在HotSpot中,底層存在一個分代GC的框架,Serial/SerialOld/ParNew/CMS都是基于該框架實(shí)現(xiàn)的,而在該框架內(nèi)的新生代收集器和年老代收集器是可以相互之間搭配使用的,這也是所謂的mix-and-match規(guī)則。但PS收集器在實(shí)現(xiàn)時,發(fā)現(xiàn)原本的分代GC框架并不適用,則最終采用了自己的特殊框架進(jìn)行了實(shí)現(xiàn),所以PS收集器并不在前面所說的那個分代GC框架中。因此,PS不能跟使用了那個框架的CMS搭配使用。

3.2、三色標(biāo)記算法

三色標(biāo)記算法是自CMS收集器后,應(yīng)用比較廣泛的一種并發(fā)標(biāo)記算法,它可以讓JVM在發(fā)生GC時,只發(fā)生短暫的STW即可實(shí)現(xiàn)存活對象標(biāo)記的一種算法。JVM中的CMS以及后續(xù)的不分代收集器,之所以可以做到低延遲的根本原因便在于此處。

三色標(biāo)記思想:在該算法中,將對象分為了黑、白、灰三種顏色,釋義如下:
黑:已經(jīng)被標(biāo)記完成,且依舊存活的對象。
灰:當(dāng)前對象已經(jīng)被標(biāo)記完成,但關(guān)聯(lián)節(jié)點(diǎn)(屬性成員)還未標(biāo)記的對象。
白:未曾標(biāo)記過的對象,或不具備引用的對象(垃圾對象)。

3.2.1、三色標(biāo)記執(zhí)行過程

廢話不多說,先上一張三色標(biāo)記的執(zhí)行過程圖:


三色標(biāo)記算法
  • 實(shí)現(xiàn)了三色標(biāo)記算法的GC收集器,在啟動時會分別創(chuàng)建:黑、白、灰三個集合,在最開始所有的對象都在白色集合中。
  • 在GC發(fā)生時,發(fā)生短暫的STW,將所有與GcRoots直接相連的對象轉(zhuǎn)入灰色集合中。
  • 之后并發(fā)執(zhí)行,對灰色集合中的對象進(jìn)行遍歷,根據(jù)可達(dá)性分析算法進(jìn)行對象存活標(biāo)記,當(dāng)一個對象的所有成員全部被標(biāo)記完成后,該對象則會被移入到黑色集合中。同時,也會將該對象中被標(biāo)記的成員從白色集合移入灰色集合中。
  • 不斷重復(fù)上一步操作,直至灰色集合徹底沒了對象為止。
  • 標(biāo)記完成所有對象后,再次觸發(fā)STW,通過write-barrier寫屏障檢測對象是否有變化,如果發(fā)生了改變則重新標(biāo)記,糾正并發(fā)標(biāo)記期間的“誤標(biāo)”。
  • 并發(fā)執(zhí)行清除工作,將白色集合中的所有對象全部回收(因為根據(jù)GCRoots節(jié)點(diǎn)進(jìn)行可達(dá)性分析后,所有的存活對象都會從白色集合移入到黑色集合中,所以依舊留在白色集合中的對象必然為垃圾對象,這些對象就是需要被回收的對象)。
  • 最終等待清除工作完成后,代表著整個GC過程結(jié)束,再把標(biāo)記復(fù)位,將所有的對象再次放入白色集合中,等待迎接下次GC的到來。

3.2.2、三色標(biāo)記-并發(fā)標(biāo)記導(dǎo)致的錯標(biāo)問題

采用三色標(biāo)記算法的GC收集器為了追求低延遲,一般在標(biāo)記完GCRoots直接關(guān)聯(lián)的對象后,就會結(jié)束STW,轉(zhuǎn)而采取并發(fā)標(biāo)記的手段對其他對象進(jìn)行標(biāo)記。但因為并發(fā)標(biāo)記是GC線程與用戶線程一起工作的,所以很有可能導(dǎo)致出現(xiàn)如下情況:

被標(biāo)記的黑色對象中,突然斷開了對另一個對象的引用,導(dǎo)致另外一個原本已經(jīng)被標(biāo)記為黑色的對象突然變?yōu)榱死?/p>

但是因為該對象已經(jīng)被標(biāo)記了,所以收集器不會對該對象進(jìn)行再次標(biāo)記,而等到清除工作發(fā)生時,因為當(dāng)前這個對象在最初是被標(biāo)記為了黑色,所以收集器也不會回收它。這種情況則被稱為三色標(biāo)記導(dǎo)致的“錯標(biāo)/誤標(biāo)/多標(biāo)”,也被稱為并發(fā)標(biāo)記產(chǎn)生的浮動垃圾。

對于該問題而言并非什么大事,因為這次錯標(biāo)產(chǎn)生的浮動垃圾,在下次GC時依舊會被回收,正所謂“躲得過初一,躲不過十五”,是垃圾早晚都會被“干掉”,這點(diǎn)在JVM中是毋庸置疑的,因此這個問題不必太過留意。

3.2.3、三色標(biāo)記-并發(fā)執(zhí)行導(dǎo)致的漏標(biāo)問題

假設(shè)在執(zhí)行三色標(biāo)記的過程中,出現(xiàn)了如下情況:

①一條用戶線程在執(zhí)行過程中,斷開了一個未標(biāo)記的白色對象連接,然后該對象又被一個已經(jīng)標(biāo)記成黑色的對象建立起了引用連接。如下圖:

三色標(biāo)記-漏標(biāo)問題-情況①

白色對象斷開了左側(cè)灰色對象的引用,又與右側(cè)的黑色對象建立了新的引用關(guān)系。

②一條用戶線程在執(zhí)行過程中,正好在GC線程標(biāo)記時,將一個灰色對象與一個未標(biāo)記的白色對象之間的引用連接斷開了,然后當(dāng)GC標(biāo)記完成這個灰色對象,將其標(biāo)記為黑色后,之前斷開的白色對象又重新與之建立起了引用關(guān)系。如下圖:

三色標(biāo)記-漏標(biāo)問題-情況②

GC標(biāo)記前,白色對象斷開了與灰色對象的引用,四秒鐘之后GC標(biāo)記灰色對象完成,而此時恰巧白色對象又重新與標(biāo)記結(jié)束后成為黑色的對象重新建立了引用關(guān)系。

而當(dāng)出現(xiàn)這兩種情況時,因為重新建立引用的白色對象“父節(jié)點(diǎn)”已經(jīng)被標(biāo)記黑色了,所以GC線程不會再次標(biāo)記該對象以及其成員對象,所以這些白色對象會被一直停留在白色集合中。最終導(dǎo)致的結(jié)果就是這些依舊存在引用的存活對象會被“誤判”為垃圾對象清除掉。而這種情況會直接影響到應(yīng)用程序的正確性,是不可接受的。

先來思考一下引起漏標(biāo)問題的原因:
條件一:灰色對象斷開了與白色對象的引用(直接引用或間接引用都可)。
條件二:已經(jīng)標(biāo)為黑色的對象重新與白色對象建立了引用關(guān)系。
只有當(dāng)一個對象同時滿足了如上兩個條件時才可發(fā)生漏標(biāo)問題。
上個簡單的代碼案例理解一下:

Object X = obj.fieldX; // 獲取obj.fieldX成員對象
obj.fieldX = null; // 將原本obj.fieldX的引用斷開
objA.fieldX = X; // 將斷開引用的X白色對象與黑色對象objA建立引用

從如上代碼角度來看,假設(shè)obj是一個灰色對象,此時先獲取它的成員fieldX并將其賦值給變量X,讓其堆中實(shí)例與變量X保持著引用關(guān)系。緊接著再將obj.fieldX置空,斷開與obj對象的引用關(guān)系,最后再與黑色對象objA建立起引用關(guān)系,最終關(guān)系如下:

灰色對象obj,白色對象obj.fieldX/X,黑色對象objA。
白色對象X在GC機(jī)制標(biāo)記灰色對象obj成員屬性之前,與灰色對象斷開了引用,然后又“勾搭”上了黑色對象objA,此刻白色對象X就會被永遠(yuǎn)停留在白色集合中,直至清除階段到來,被“誤判”為垃圾回收掉。

其實(shí)解決漏標(biāo)問題的思路也挺簡單的,和之前《并發(fā)編程》中解決線程安全問題一樣,線程安全問題是存在三個必要條件的,破壞掉其中任意條件后,線程安全問題就不會出現(xiàn)。而剛剛前面也分析過,對象漏標(biāo)的問題也存在兩個必要條件,那么我們也只需要破壞掉其中任意條件即可。比如上述案例中,我們只要能夠通過特殊手段記錄一下X對象,然后將它作為灰色對象再遍歷標(biāo)記一次即可。

  • 采用三色標(biāo)記算法的收集器又是如何具體解決漏標(biāo)問題的呢?
  • CMS:增量更新 + 寫屏障
  • G1:STAB + 寫屏障
  • ZGC:讀屏障

在本篇中,先對CMS解決漏標(biāo)的方案進(jìn)行分析,對于G1、ZGC收集器的漏標(biāo)問題解決則放到下篇文章中進(jìn)行闡述。

3.2.4、CMS解決漏標(biāo)問題:增量更新 + 寫屏障

在了解寫屏障之前,我們首先來看看HotSpot中為對象成員賦值的實(shí)現(xiàn),大體邏輯如下:

void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 賦值操作:新值替換老值
} 

而所謂的寫屏障,則是指在賦值操作前后加入一些邏輯處理(類似于SpringAOP面向切面前后置處理的思想),如下:

void oop_field_store(oop* field, oop new_value) {
    pre_write_barrier(field); // 寫前屏障
    *field = new_value; // 賦值操作:新值替換老值
    post_write_barrier(field, value);  // 寫后屏障
} 

而CMS收集器則是通過在寫屏障的后置處理中,實(shí)現(xiàn)了增量更新的邏輯,從而解決了漏標(biāo)問題。

增量更新(Increment Update)是專門針對于對象新增引用的,當(dāng)一個未標(biāo)記的白色對象被其他對象重新引用時,這個白色對象會被記錄下來,如下:

// 寫后屏障
void post_write_barrier(oop* field, oop new_value) {  
  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
      remark_set.add(new_value); // 記錄新引用的對象(白色對象)
  }
}

從如上源碼中可以觀察出:對于賦值的新增引用,會在寫后屏障中會被放到一個特定的集合記錄,等并發(fā)標(biāo)記階段的GCRoots遍歷標(biāo)記完成后,在重新標(biāo)記階段會去找到集合里面的引用,再把源頭標(biāo)記為灰色,然后重新去掃描標(biāo)記這些對象。

CMS通過寫屏障+增量更新這種手段,破壞了之前分析漏標(biāo)問題時的第二個條件:已經(jīng)標(biāo)為黑色的對象重新與白色對象建立了引用關(guān)系。
通過增量更新的手段,會將這些重新建立了引用的“源頭”再次恢復(fù)為灰色對象,然后在重新標(biāo)記階段會再次標(biāo)記,同時為了避免重新標(biāo)記階段時再次發(fā)生漏標(biāo)問題,所以重新標(biāo)記階段是必須要發(fā)生STW的。
HotSpot中寫屏障的具體實(shí)現(xiàn)可參考:《BarrierSet源碼分析》。

3.3、跨代引用

跨代引用是指年老代空間中的對象引用了新生代的對象,或者新生代中的對象引用了年老代中的對象。面對這種情況,在進(jìn)行可達(dá)性分析掃描存活對象時,不可能從新生代一直掃描至年老代的,因為這樣就會出現(xiàn)整堆掃描的情況,效率必然會很低。

在HotSpot虛擬機(jī)中,為了解決跨代引用的問題,會專門在內(nèi)存中開辟一塊小空間用于維護(hù)這些特殊的引用,從而達(dá)到讓GC不必掃描整個堆空間的目的。而開辟的這塊小空間則被稱為記憶集、卡表

3.1、記憶集(Remember Set)

我們都知道在發(fā)生新生代GC時都會通過根可達(dá)算法先判斷垃圾對象,之后再對非存活對象進(jìn)行統(tǒng)一回收,但是如果有年老代對象引用了新生代對象,那么根據(jù)根可達(dá)算法的特性,年老代也會被加入掃描范圍,這樣下來一次新生代的GC代價太大。所以為了解決跨代引用的問題,在新生代引入了記錄集的數(shù)據(jù)結(jié)構(gòu),記錄從非收集區(qū)到收集區(qū)的引用指針集合,避免在通過根可達(dá)算法判斷對象存活時把整個老年代加入掃描范圍。

GC時,GC收集器只需通過記憶集判斷出某一塊非收集區(qū)域是否存在指向收集區(qū)域的指針即可,無需進(jìn)行詳細(xì)的根搜索過程。
記憶集可根據(jù)不同的記憶粒度實(shí)現(xiàn):
①字寬/字長精度:精確到每個字寬(32bit/64bit),每一個跨代引用指針
②對象精度:精確到每個對象,對象的字段中包含跨代引用指針
③卡精度:精準(zhǔn)到每一塊內(nèi)存區(qū)域,內(nèi)存區(qū)域中有對象存在跨代指針

3.2、卡表(Card Table)

卡表是記憶集第三種精度的實(shí)現(xiàn),也是HotSpot虛擬機(jī)中記憶集的實(shí)現(xiàn)方式,卡表中記錄中記憶集的記錄精度、與堆內(nèi)存區(qū)域的映射關(guān)系等。

在HotSpot中卡表是使用一個字節(jié)數(shù)組實(shí)現(xiàn):CARD_TABLE[this addredd >>9]=0,數(shù)組中每個元素對應(yīng)著其標(biāo)識的內(nèi)存區(qū)域,稱為卡頁,hotSpot使用的卡頁大小為2^9 即512字節(jié),也就是說內(nèi)存中每連續(xù)的512字節(jié)會被當(dāng)作一個卡頁作為卡表的一個元素。

如果有年老代的對象引用了新生代的對象,那么該新生代對象所在區(qū)域?qū)?yīng)的卡頁元素設(shè)置為1,反之則為0。(G1以后的GC收集器不分代,所以G1以后的記憶集不是通過數(shù)組實(shí)現(xiàn)的,而是通過哈希表結(jié)構(gòu)實(shí)現(xiàn))。

JVM對于卡頁的維護(hù)也是通過寫屏障的方式。

四、GC日志解讀

對于GC機(jī)制而言,這塊區(qū)域是程序員做JVM調(diào)優(yōu)的關(guān)鍵,而調(diào)優(yōu)前必然得讀懂GC發(fā)生后產(chǎn)生的日志。在JVM中GC日志相關(guān)的參數(shù)如下:

  • -XX:+PrintGC-verbose:gc:打印GC日志
  • -XX:+PrintGCDetails:打印GC的詳細(xì)日志
  • -XX:+PrintGCTimeStamps:輸出GC的時間戳(以基準(zhǔn)時間的形式)
  • -XX:+PrintGCDateStamps:輸出GC的時間戳(以日期的形式)
  • -XX:+PrintHeapAtGC:在發(fā)生GC的前后打印出堆的信息
  • -Xloggc:/xxx/xxx/xx.log:GC日志文件的保存路徑

其中-XX:+PrintGC-verbose:gc參數(shù)只能輸出GC時堆空間總體的變化信息,來個簡單的案例理解一下:

// 啟動參數(shù):-Xms8M -Xmx8M -XX:+PrintGC
public class GC {
    static void newObject(){
        for (int i = 0; i <= 10000; i++)
            new Object();
    }

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            newObject();
        }
    }
}

執(zhí)行上述案例后,你的控制臺中會得到如下日志:

[GC (Allocation Failure)  1527K->868K(7680K), 0.0011957 secs]
[GC (Allocation Failure)  1924K->1201K(7680K), 0.0032349 secs]
......

我們從輸出的日志中隨意找出一條來用于分析,如下:

[GC[1] (Allocation Failure)[2] 1527K[3]->868K[4](7680K)[5], 0.0011957 secs[6]]

該日志只會大概的將堆空間的總體情況打印出來,日志信息解讀如下:

  • [1]:此次GC的類型
    • GC:表示Young GC,新生代發(fā)生的GC類型
    • Full GC:全局GC,新生代、年老代以及元空間的GC類型
  • [2]:此次GC產(chǎn)生的原因
    • Allocation Failure:新創(chuàng)建的對象分配失敗導(dǎo)致的GC
    • Metadata GC Threshold:元空間數(shù)據(jù)達(dá)到分配的空間閾值導(dǎo)致的GC
    • System.gc():程序中手動通過System.gc()觸發(fā)的GC
    • ......
  • [3]:GC發(fā)生前,堆的已用空間大小
  • [4]:GC發(fā)生后,堆的已用空間大小
  • [5]:堆空間的總大小
  • [6]:GC持續(xù)的時間

如下圖:

-XX:+PrintGC日志解讀

整條GC日志的規(guī)律為:GC類型+GC原因+堆空間描述+耗時描述。

4.1、GC日志詳細(xì)信息解讀

在前面提到過-XX:+PrintGC參數(shù)只能輸出GC時堆的總體變化信息,這種日志對于線上遇到突發(fā)狀況而言,幾乎是很難從中獲取到有用信息的。因此,一般而言線上都會采用-XX:+PrintGCDetails參數(shù)獲取GC的詳細(xì)日志信息。案例如下:

// 啟動參數(shù):-Xms8M -Xmx8M -XX:+PrintGCDetails
public class GC {
    // 作為GC Roots
    static List<Object> listObject = new ArrayList<>();
    
    // 往新生代空間中填充對象
    static void newObject(){
        for (int i = 0; i <= 100000; i++)
            new Object();
    }
    
    // 創(chuàng)建的對象與GCRoots保持引用,足以對象晉升年老代空間
    static void oldObject(){
        for (int i = 0; i <= 10000; i++)
            listObject.add(new Object());
    }

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            newObject();
            oldObject();
        }
    }
}

運(yùn)行上述程序后可以得到如下日志信息(為了方便觀察已手動排版):

[GC (Allocation Failure) [PSYoungGen: 1527K->492K(2048K)]
    1527K->892K(7680K), 0.0038507 secs] 
    [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1548K->483K(2048K)]
    1948K->1174K(7680K), 0.0009940 secs] 
    [Times: user=0.00 sys=0.00, real=0.00 secs] 
    
省略大部分相同類型的日志.......

[Full GC (Ergonomics) [PSYoungGen: 2016K->0K(2048K)] 
    [ParOldGen: 4822K->4807K(5632K)] 6839K->4807K(7680K), 
    [Metaspace: 3625K->3625K(1056768K)], 0.0393051 secs] 
    [Times: user=0.06 sys=0.00, real=0.04 secs]
[Full GC (Allocation Failure)[PSYoungGen: 693K->693K(2048K)] 
    [ParOldGen: 5245K->5226K(5632K)] 5938K->5919K(7680K), 
    [Metaspace: 3626K->3626K(1056768K)], 0.0312005 secs] 
    [Times: user=0.03 sys=0.00, real=0.03 secs]
Heap
 PSYoungGen total 2048K, used 754K [0x00000000ffd80000,...)
  eden space 1536K, 49% used [0x00000000ffd80000,...)
  from space 512K, 0% used [0x00000000fff00000,...)
  to   space 512K, 0% used [0x00000000fff80000,...)
 ParOldGen total 5632K, used 5226K [0x00000000ff800000,...)
  object space 5632K, 92% used [0x00000000ff800000,...)
 Metaspace used 3657K, capacity 4540K, committed 4864K, reserved 1056768K
  class space used 402K, capacity 428K, committed 512K, reserved 1048576K

觀察如上GC日志可以看出:在該程序運(yùn)行之后,除開觸發(fā)了新生代GC外,在后期隨著存活的對象越來越多,最終也觸發(fā)了FullGC

同時在日志的最后,也會將每個Java內(nèi)存空間中的占用情況顯示出來,如新生代中eden、form、to區(qū)占用情況,年老代空間占用情況,元數(shù)據(jù)空間占用情況等。

接下來我們從普通GC日志出發(fā),對上述日志中的信息進(jìn)行闡述。

4.1.1、YoungGC日志詳解

先從上述日志中摘錄一條普通GC日志下來:

[GC[1] (Allocation Failure) [2][PSYoungGen[3]: 1527K[4]->492K[5](2048K[6])]
1527K[7]->892K[8](7680K[9]), 0.0038507 secs[10]]
[Times: user=0.00[11] sys=0.00[12], real=0.00 [13]secs]

對于這條GC日志解讀如下:

  • [1]:此次GC的類型(普通的Young GC
  • [2]:此次GC產(chǎn)生的原因(分配失敗導(dǎo)致的GC)
  • [3]:負(fù)責(zé)此次GC的收集器與GC類型(PS的新生代GC)
  • [4]:GC發(fā)生前,新生代空間的已用大?。?code>1527KB)
  • [5]:GC回收后,新生代空間的已用大?。?code>492KB)
  • [6]:新生代空間分配到的總大?。?code>2048KB)
  • [7]:GC發(fā)生前,Java堆空間的已用大?。?code>1527KB)
  • [8]:GC回收后,Java堆空間的已用大?。?code>892KB)
  • [9]:Java堆空間分配到的總大?。?code>7680KB)
  • [10]:本次GC過程的總耗時(0.0038507秒)
  • [11]:本次GC過程的用戶耗時(0
    • 這里是因為太短暫了,因此無法精準(zhǔn)出具體的耗時,而并非真的為0。
  • [12]:本次GC過程的系統(tǒng)耗時(0
  • [13]:本次GC過程的實(shí)際耗時(0
YoungGC日志詳解

整條YoungGC的日志如上圖所示,其中規(guī)律為:
GC類型+GC原因+GC收集器+新生代描述+堆空間描述+耗時描述。

4.1.2、FullGC日志詳解

同樣的再摘錄一條FullGC日志,如下:

[Full GC[1] (Ergonomics[2]) [PSYoungGen[3]: 2016K[4]->0K[5](2048K)[6]]
[ParOldGen:[7] 4822K[8]->4807K[9](5632K)[10]] 6839K[11]->4807K[12](7680K)[13],
[Metaspace:[14] 3625K[15]->3625K[16](1056768K)[17]], 0.0393051 secs[18]]
[Times: user=0.06[19] sys=0.00[20], real=0.04 secs[21]]

  • [1]:此次GC的類型(全局的Full GC
  • [2]:此次GC產(chǎn)生的原因(預(yù)計下次分配存放不下觸發(fā)的GC)
  • [3]:負(fù)責(zé)此次新生代GC的收集器(PS)
  • [4]:GC發(fā)生前,新生代空間的已用大小(2016KB
  • [5]:GC回收后,新生代空間的占用大小(0KB
  • [6]:新生代空間分配到的總大?。?code>2048KB)
  • [7]:負(fù)責(zé)此次年老代GC的收集器(PO)
  • [8]:GC發(fā)生前,年老代空間的已用大?。?code>4822KB)
  • [9]:GC回收后,年老代空間的占用大?。?code>4807KB)
  • [10]:年老代空間分配到的總大?。?code>5632KB)
  • [11]:GC發(fā)生前,Java堆空間的已用大小(6839KB
  • [12]:GC回收后,Java堆空間的已用大?。?code>4807KB)
  • [13]:Java堆空間分配到的總大?。?code>7680KB)
  • [14]:回收區(qū)域(Metaspace元數(shù)據(jù)空間)
  • [15]:GC發(fā)生前,元數(shù)據(jù)空間的已用大?。?code>3625KB)
  • [16]:GC回收后,元數(shù)據(jù)空間的占用大?。?code>3625KB)
  • [17]:元數(shù)據(jù)空間分配到的總大?。?code>1056768KB)
  • [18]:本次GC過程的總耗時(0.0393051秒)
  • [19]:本次GC過程的用戶耗時(0
  • [20]:本次GC過程的系統(tǒng)耗時(0
  • [21]:本次GC過程的實(shí)際耗時(0.04秒)
FullGC日志詳解

每條FullGC的日志如上圖所示,其中規(guī)律為:
GC類型+GC原因+新生代描述+年老代描述+堆空間描述+元數(shù)據(jù)空間+耗時描述。

4.1.3、誘發(fā)GC的原因

之前的日志中曾見到過幾種導(dǎo)致GC的原因,如Allocation Failure、Ergonomics、Metadata GC Threshold等,那么誘發(fā)GC的原因究竟有多少種呢?其實(shí)在HotSpot源碼中,運(yùn)行時觸發(fā)GC的原因都已經(jīng)定義好了,在/src/share/vm/gc_interface/gcCause.cpp文件中定義了(基于OPenJDK1.8源碼),如下:

#include "precompiled.hpp"
#include "gc_interface/gcCause.hpp"

const char* GCCause::to_string(GCCause::Cause cause) {
  switch (cause) {
    case _java_lang_system_gc:
      return "System.gc()";

    case _full_gc_alot:
      return "FullGCAlot";

    case _scavenge_alot:
      return "ScavengeAlot";

    case _allocation_profiler:
      return "Allocation Profiler";

    case _jvmti_force_gc:
      return "JvmtiEnv ForceGarbageCollection";

    case _gc_locker:
      return "GCLocker Initiated GC";

    case _heap_inspection:
      return "Heap Inspection Initiated GC";

    case _heap_dump:
      return "Heap Dump Initiated GC";

    case _no_gc:
      return "No GC";

    case _allocation_failure:
      return "Allocation Failure";

    case _tenured_generation_full:
      return "Tenured Generation Full";

    case _metadata_GC_threshold:
      return "Metadata GC Threshold";

    case _cms_generation_full:
      return "CMS Generation Full";

    case _cms_initial_mark:
      return "CMS Initial Mark";

    case _cms_final_remark:
      return "CMS Final Remark";

    case _cms_concurrent_mark:
      return "CMS Concurrent Mark";

    case _old_generation_expanded_on_last_scavenge:
      return "Old Generation Expanded On Last Scavenge";

    case _old_generation_too_full_to_scavenge:
      return "Old Generation Too Full To Scavenge";

    case _adaptive_size_policy:
      return "Ergonomics";

    case _g1_inc_collection_pause:
      return "G1 Evacuation Pause";

    case _g1_humongous_allocation:
      return "G1 Humongous Allocation";

    case _last_ditch_collection:
      return "Last ditch collection";

    case _last_gc_cause:
      return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";

    default:
      return "unknown GCCause";
  }
  ShouldNotReachHere();
}

HotSpot源碼看來,其實(shí)導(dǎo)致GC被觸發(fā)的原因有很多種,在GC日志信息中,可能出現(xiàn)的總計有二十余種,下面依次簡單介紹一下

  • System.gc():Java程序中手動調(diào)用System.gc()方法觸發(fā)的GC。
  • FullGCAlot:定期觸發(fā)的GC(JDK內(nèi)測專屬,JVM開發(fā)時使用)。
  • ScavengeAlot:定期觸發(fā)的GC(JDK內(nèi)測專屬,JVM開發(fā)時使用)。
  • Allocation Profiler:使用-Xaprof參數(shù)運(yùn)行程序,在JVM結(jié)束時會觸發(fā)的GC(JFK1.8被棄用了)。
  • JvmtiEnv ForceGarbageCollection:強(qiáng)制調(diào)用本地方法庫中的native方法:ForceGarbageCollection(jvmtiEnv* env)觸發(fā)的GC。
  • GCLocker Initiated GC:如果線程執(zhí)行在 JNI 臨界區(qū)時,剛好需要進(jìn)行 GC,此時GCLocker將會阻止GC的發(fā)生,同時阻止其他線程進(jìn)入JNI臨界區(qū),直到最后一個線程退出臨界區(qū)時觸發(fā)一次GC。
  • Heap Inspection Initiated GC:通過jmap命令進(jìn)行堆檢測時觸發(fā)的GC。
    • 堆檢測命令:jmap -histo:live <pid>
  • Heap Dump Initiated GC:通過jmap命令進(jìn)行堆轉(zhuǎn)儲時觸發(fā)的GC。
    • 堆轉(zhuǎn)儲命令:jmap -dump:live,format=b,file=heap.out <pid>
  • WhiteBox Initiated Young GC:測試時主動觸發(fā)的Young GC(需要增加WhiteBoxAgent才能使用)。
  • Update Allocation Context Stats:這個GC僅用于獲取更新的分配上下文統(tǒng)計信息。
  • Allocation Failure:對象分配時內(nèi)存不足導(dǎo)致分配失敗觸發(fā)的GC。
  • Tenured Generation Full:年老代空間內(nèi)存不足觸發(fā)的GC。
  • Metadata GC Threshold:元數(shù)據(jù)空間內(nèi)存不足觸發(fā)的GC。
  • CMS收集器相關(guān)的GC日志信息:
    • No GC:用于表示CMS的并發(fā)標(biāo)記階段。
    • CMS Generation Full:CMS發(fā)生FullGC.
    • CMS Initial Mark:CMS初始標(biāo)記階段的日志信息。
    • CMS Final Remark:CMS重新標(biāo)記階段的日志信息。
    • CMS Concurrent Mark:CMS并發(fā)標(biāo)記階段的日志信息。
  • 沒弄明白的兩個:
    • Old Generation Expanded On Last Scavenge
    • Old Generation Too Full To Scavenge
    • 如有明白這兩玩意兒的評論區(qū)留言。
  • Ergonomics:一般出現(xiàn)在PS+PO組合中,空間分配擔(dān)保時觸發(fā)的GC。
  • G1收集器相關(guān)的GC日志信息:
    • G1 Evacuation Pause:G1中沒有空閑的region區(qū)導(dǎo)致分配失敗時觸發(fā)的GC。
    • G1 Humongous Allocation:沒有Humongous區(qū)分配大對象時觸發(fā)的GC。
  • Last ditch collection:在元數(shù)據(jù)空間分配數(shù)據(jù)時,分配失敗且無法繼續(xù)擴(kuò)展內(nèi)存時觸發(fā)的GC。
  • ILLEGAL VALUE - last gc cause - ILLEGAL VALUE:正常情況下該信息是看不到的。
  • unknown GCCause:未知(未定義)的原因觸發(fā)的GC。

五、GC分代篇總結(jié)

在本章中,我們依次從GC的一些基礎(chǔ)概念,到分代收集器、各款收集器收集過程、CMS收集器及其執(zhí)行過程、三色標(biāo)記算法、三色標(biāo)記-漏標(biāo)/多標(biāo)問題、YoungGC、FullGC日志解讀、GC誘發(fā)原因等內(nèi)容進(jìn)行全面闡述。

在JVM的GC體系中,其實(shí)并不存在所謂的最好GC器,不同的場景下采用合適的GC收集器,才能在最大程度上追求最優(yōu)的方案。各款GC收集器對比如下:

GC收集器 GC屬性 作用區(qū)域 GC算法 特性 應(yīng)用場景
Serial 串行回收 新生代 復(fù)制算法 響應(yīng)速度優(yōu)先 單核機(jī)器/client程序
Serial Old 串行回收 年老代 標(biāo)-整算法 響應(yīng)速度優(yōu)先 單核機(jī)器/client程序
ParNew 并行回收 新生代 復(fù)制算法 吞吐量優(yōu)先 計算多/交互少的程序
Parallel Scavenge 并行回收 新生代 復(fù)制算法 吞吐量優(yōu)先 計算多/交互少的程序
Parallel Old 并行回收 年老代 標(biāo)-整算法 吞吐量優(yōu)先 計算多/交互少的程序
Parallel Old 并行/并發(fā)回收 年老代 標(biāo)-清算法 響應(yīng)速度優(yōu)先 交互多/計算少的程序
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容