引言
在《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ū)兩大類:

如上圖,分代堆空間中會分為新生代與年老代兩個區(qū)域,而新生代又會分為
Eden*1、Survivor*2三塊。其中新生代采用復(fù)制算法,HotSpot中因為調(diào)整了Eden與Survivor區(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等,如下:

在上圖中共有十款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收集器可以搭配使用,所以一共存在六種搭配方案:
| 新生代 | 年老代 |
|---|---|
| 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)存碎片的FullGC(MSC的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組合絕對是你的最佳選擇。

我們再一次將目光聚集在這張圖上,需要值得注意的是:在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í)行過程圖:

- 實(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ù)的時間
如下圖:

整條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的日志如上圖所示,其中規(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的日志如上圖所示,其中規(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>
- 堆轉(zhuǎn)儲命令:
-
WhiteBox Initiated Young GC:測試時主動觸發(fā)的Young GC(需要增加WhiteBox的Agent才能使用)。 -
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 ScavengeOld 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)先 | 交互多/計算少的程序 |







