JVM性能調優(yōu)實踐—G1垃圾收集器全視角解析
點擊上方藍色字體,選擇“設為星標”

本文將總結一下GC的種類,然后側重總結下G1(Garbage-First)垃圾收集器的分代,結合open-jdk源碼分析下重要算法如SATB,重要存儲結構如CSet、RSet、TLAB、PLAB、Card Table等。最后會再梳理下G1 GC的YoungGC,MixedGC收集過程。
GC的分類
GC的主要回收區(qū)域就是年輕代(young gen)、老年代(tenured gen)、持久區(qū)(perm gen),在jdk8之后,perm gen消失,被替換成了元空間(Metaspace),元空間會在普通的堆區(qū)進行分配。垃圾收集為了提高效率,采用分代收集的方式,對于不同特點的回收區(qū)域使用不同的垃圾收集器。系統(tǒng)正常運行情況young是比較頻繁的,full gc會觸發(fā)整個heap的掃描和回收。在G1垃圾收集器中,最好的優(yōu)化狀態(tài)就是通過不斷調整分區(qū)空間,避免進行full gc,可以大幅提高吞吐量。下面會詳細介紹。
串行垃圾回收器
JDK 1.3之前的垃圾回收器,單線程回收,并且會有stop theworld(下文會簡稱STW),也即GC時,暫停所有用戶線程。其運行方式是單線程的,適合Client模式的應用,適合單CPU環(huán)境。串行的垃圾收集器有兩種,Serial以及SerialOld,一般會搭配使用。新生代使用Serial采取復制算法,老年代使用Serial Old采取標記整理算法。Client應用或者命令行程序可以,通過-XX:+UseSerialGC可以開啟上述回收模式。
Serial:用于新生代垃圾收集,復制算法
SerialOld:用于老年代垃圾收集,標記整理算法
并行垃圾回收器
整體來說,并行垃圾回收相對于串行,是通過多線程運行垃圾收集的。也會stop-the-world。適合Server模式以及多CPU環(huán)境。一般會和jdk1.5之后出現(xiàn)的CMS搭配使用。并行的垃圾回收器有以下幾種:
ParNew:Serial收集器的多線程版本,默認開啟的收集線程數(shù)和cpu數(shù)量一樣,運行數(shù)量可以通過修改ParallelGCThreads設定。用于新生代收集,復制算法。使用-XX:+UseParNewGC,和Serial Old收集器組合進行內存回收。
Parallel Scavenge: 關注吞吐量,吞吐量優(yōu)先,吞吐量=代碼運行時間/(代碼運行時間+垃圾收集時間),也就是高效率利用cpu時間,盡快完成程序的運算任務 可以設置最大停頓時間MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。如果設置了-XX:+UseAdaptiveSizePolicy參數(shù),則隨著GC,會動態(tài)調整新生代的大小,Eden,Survivor比例等,以提供最合適的停頓時間或者最大的吞吐量。用于新生代收集,復制算法。通過-XX:+UseParallelGC參數(shù),Server模式下默認提供了其和SerialOld進行搭配的分代收集方式。
Parllel Old:Parallel Scavenge的老年代版本。JDK 1.6開始提供的。在此之前Parallel Scavenge的地位也很尷尬,而有了Parllel Old之后,通過-XX:+UseParallelOldGC參數(shù)使用Parallel Scavenge + Parallel Old器組合進行內存回收。
并發(fā)標記掃描垃圾回收器(CMS)
CMS(Concurrent Mark Sweep)基于“標記—清除”算法,用于老年代,所以其關注點在于減少“pause time”也即因垃圾回收導致的stop the world時間。對于重視服務的響應速度的應用可以使用CMS。因為CMS是“并發(fā)”運行的,也即垃圾收集線程可以和用戶線程同時運行。缺點就是會產(chǎn)生內存碎片。CMS的回收分為幾個階段:
初始標記:標記一下GC Roots能直接關聯(lián)到的對象,會“Stop The World”
并發(fā)標記:GC Roots Tracing,可以和用戶線程并發(fā)執(zhí)行。
重新標記:標記期間產(chǎn)生的對象存活的再次判斷,修正對這些對象的標記,執(zhí)行時間相對并發(fā)標記短,會“Stop The World”。
并發(fā)清除:清除對象,可以和用戶線程并發(fā)執(zhí)行。
CMS最主要解決了pause time,但是會占用CPU資源,犧牲吞吐量。CMS默認啟動的回收線程數(shù)是(CPU數(shù)量+3)/ 4,當CPU < 4個時,會影響用戶線程的執(zhí)行。另外一個缺點就是內存碎片的問題了,碎片會給大對象的內存分配造成麻煩,如果老年代的可用的連續(xù)空間也無法分配時,會觸發(fā)full gc。并且full gc時如果發(fā)生young gc會被young gc打斷,執(zhí)行完young gc之后再繼續(xù)執(zhí)行full gc。
-XX:UseConcMarkSweepGC參數(shù)可以開啟CMS,年輕代使用ParNew,老年代使用CMS,同時Serial Old收集器將作為CMS收集器出現(xiàn)Concurrent Mode Failure失敗后的后備收集器使用。
G1垃圾收集器
G1(Garbage-First)是在JDK 7u4版本之后發(fā)布的垃圾收集器,并在jdk9中成為默認垃圾收集器。通過“-XX:+UseG1GC”啟動參數(shù)即可指定使用G1 GC。從整體來說,G1也是利用多CPU來縮短stop the world時間,并且是高效的并發(fā)垃圾收集器。但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因為G1中的垃圾收集區(qū)域是“分區(qū)”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年輕代的ygc,全堆掃描的fullgc外,還有包含所有年輕代以及部分老年代Region的MixedGC。G1的優(yōu)勢還有可以通過調整參數(shù),指定垃圾收集的最大允許pause time。下面會詳細闡述下G1分區(qū)以及分代的概念,以及G1 GC的幾種收集過程的分類。
G1分區(qū)的概念
在G1之前的垃圾收集器,將堆區(qū)主要劃分了Eden區(qū),Old區(qū),Survivor區(qū)。其中對于Eden,Survivor對回收過程來說叫做“年輕代垃圾收集”。并且年輕代和老年代都分別是連續(xù)的內存空間。
G1將堆分成了若干Region,以下和”分區(qū)”代表同一概念。Region的大小可以通過G1HeapRegionSize參數(shù)進行設置,其必須是2的冪,范圍允許為1Mb到32Mb。JVM的會基于堆內存的初始值和最大值的平均數(shù)計算分區(qū)的尺寸,平均的堆尺寸會分出約2000個Region。分區(qū)大小一旦設置,則啟動之后不會再變化。如下圖簡單畫了下G1分區(qū)模型。

Eden regions(年輕代-Eden區(qū))
Survivor regions(年輕代-Survivor區(qū))
Old regions(老年代)
Humongous regions(巨型對象區(qū)域)
Free resgions(未分配區(qū)域,也會叫做可用分區(qū))-上圖中空白的區(qū)域
關于分區(qū)有幾個重要的概念:
G1還是采用分代回收,但是不同的分代之間內存不一定是連續(xù)的,不同分代的Region的占用數(shù)也不一定是固定的(不建議通過相關選項顯式設置年輕代大小。會覆蓋暫停時間目標。)。年輕代的Eden,Survivor數(shù)量會隨著每一次GC發(fā)生相應的改變。
分區(qū)是不固定屬于哪個分代的,所以比如一次ygc過后,原來的Eden的分區(qū)就會變成空閑的可用分區(qū),隨后也可能被用作分配巨型對象,成為H區(qū)等。
G1中的巨型對象是指,占用了Region容量的50%以上的一個對象。Humongous區(qū),就專門用來存儲巨型對象。如果一個H區(qū)裝不下一個巨型對象,則會通過連續(xù)的若干H分區(qū)來存儲。因為巨型對象的轉移會影響GC效率,所以并發(fā)標記階段發(fā)現(xiàn)巨型對象不再存活時,會將其直接回收。ygc也會在某些情況下對巨型對象進行回收。通過上圖可以看出,分區(qū)可以有效利用內存空間,因為收集整體是使用“標記-整理”,Region之間基于“復制”算法,GC后會將存活對象復制到可用分區(qū)(未分配的分區(qū)),所以不會產(chǎn)生空間碎片。
G1類似CMS,也會在比如一次fullgc中基于堆尺寸的計算重新調整(增加)堆的空間。但是相較于執(zhí)行fullgc,G1 GC會在無法分配對象或者巨型對象無法獲得連續(xù)分區(qū)來分配空間時,優(yōu)先嘗試擴展堆空間來獲得更多的可用分區(qū)。原則上就是G1會計算執(zhí)行GC的時間,并且極力減少花在GC上的時間(包括ygc,mixgc),如果可能,會通過不斷擴展堆空間來滿足對象分配、轉移的需要。
因為G1提供了“可預測的暫停時間”,也是基于G1的啟發(fā)式算法,所以G1會估算年輕代需要多少分區(qū),以及還有多少分區(qū)要被回收。ygc觸發(fā)的契機就是在Eden分區(qū)數(shù)量達到上限時。一次ygc會回收所有的Eden和survivor區(qū)。其中存活的對象會被轉移到另一個新的survivor區(qū)或者old區(qū),如果轉移的目標分區(qū)滿了,會再將可用區(qū)標記成S或者O區(qū)。
G1 中的重要數(shù)據(jù)結構、算法
在提及G1的垃圾收集過程時,需要理解幾個G1的重要的分區(qū)內部的詳細數(shù)據(jù)結構、以及核心算法。
TLAB(Thread Local Allocation Buffer)本地線程緩沖區(qū)
G1 GC會默認會啟用Tlab優(yōu)化。其作用就是在并發(fā)情況下,基于CAS的獨享線程(mutator threads)可以優(yōu)先將對象分配在一塊內存區(qū)域(屬于Java堆的Eden中),只是因為是Java線程獨享的內存區(qū),沒有鎖競爭,所以分配速度更快,每個Tlab都是一個線程獨享的。如果待分配的對象被判斷是巨型對象,則不使用TLAB。下面把TLAB分配對象內存的open jdk部分源碼附上,有助理解。
HeapWord* G1CollectedHeap::allocate_new_tlab(size_t min_size,
size_t requested_size,
size_t* actual_size) {
assert_heap_not_locked_and_not_at_safepoint();
assert(!is_humongous(requested_size), "we do not allow humongous TLABs");
return attempt_allocation(min_size, requested_size, actual_size);
}
inline HeapWord* G1CollectedHeap::attempt_allocation(size_t min_word_size,
size_t desired_word_size,
size_t* actual_word_size) {
assert_heap_not_locked_and_not_at_safepoint();
// 排除巨型對象
assert(!is_humongous(desired_word_size), "attempt_allocation() should not "
"be called for humongous allocation requests");
// 在當前的region分配
HeapWord* result = _allocator->attempt_allocation(min_word_size, desired_word_size, actual_word_size);
// 可用空間不夠,申請新的region分配
if (result == NULL) {
*actual_word_size = desired_word_size;
// 可能存在多線程申請,所以通過加鎖的方式申請,如果young區(qū)沒有超出閥值,則會獲取新的region
result = attempt_allocation_slow(desired_word_size);
}
// 判斷沒有因gc導致堆locked
assert_heap_not_locked();
if (result != NULL) {
assert(*actual_word_size != 0, "Actual size must have been set here");
// 臟化年輕代的card(卡片)數(shù)據(jù)
dirty_young_block(result, *actual_word_size);
} else {
*actual_word_size = 0;
}
return result;
}
PLAB(Promotion Local Allocation Buffer) 晉升本地分配緩沖區(qū)
在ygc中,對象會將全部Eden區(qū)存貨的對象轉移(復制)到S區(qū)分區(qū)。也會存在S區(qū)對象晉升(Promotion)到老年代。這個決定晉升的閥值可以通過MaxTenuringThreshold設定。晉升的過程,無論是晉升到S還是O區(qū),都是在GC線程的PLAB中進行。每個GC線程都有一個PLAB。
Collection Sets(CSets)待收集集合
GC中待回收的region的集合。CSet中可能存放著各個分代的Region。CSet中的存活對象會在gc中被移動(復制)。GC后CSet中的region會成為可用分區(qū)。
Card Table 卡表
將Java堆劃分為相等大小的一個個區(qū)域,這個小的區(qū)域(一般size在128-512字節(jié))被當做Card,而Card Table維護著所有的Card。Card Table的結構是一個字節(jié)數(shù)組,Card Table用單字節(jié)的信息映射著一個Card。當Card中存儲了對象時,稱為這個Card被臟化了(dirty card)。對于一些熱點Card會存放到Hot card cache。同Card Table一樣,Hot card cache也是全局的結構。
Remembered Sets(RSets)已記憶集合
已記憶集合在每個分區(qū)中都存在,并且每個分區(qū)只有一個RSet。其中存儲著其他分區(qū)中的對象對本分區(qū)對象的引用,是一種points-in結構。ygc的時候,只要掃描RSet中的其他old區(qū)對象對于本young區(qū)的引用,不需要掃描所有old區(qū)。mixed gc時,掃描Old區(qū)的RSet中,其他old區(qū)對于本old分區(qū)的引用,一樣不用掃描所有的old區(qū)。提高了GC效率。因為每次GC都會掃描所有young區(qū)對象,所以RSet只有在掃描old引用young,old引用old時會被使用。
為了防止RSet溢出,對于一些比較“Hot”的RSet會通過存儲粒度級別來控制。RSet有三種粒度,對于“Hot”的RSet在存儲時,根據(jù)細粒度的存儲閥值,可能會采取粗粒度。
這三種粒度的RSet都是通過PerRegionTable來維護內部數(shù)據(jù)的。可以查看其部分源碼如下:
class PerRegionTable: public CHeapObj {
friend class OtherRegionsTable;
friend class HeapRegionRemSetIterator;
HeapRegion* _hr; // 來自其他分區(qū)的引用
CHeapBitMap _bm; // card索引存放的位圖
jint _occupied; // 已占用的容量
// next pointer for free/allocated 'all' list
PerRegionTable* _next;
// prev pointer for the allocated 'all' list
PerRegionTable* _prev;
// next pointer in collision list
PerRegionTable * _collision_list_next;
// Global free list of PRTs
static PerRegionTable* volatile _free_list;

下面是三種粒度級別,以及對應的簡要數(shù)據(jù)結構:細粒度(fine),其PerRegionTable存儲了所有對于本Resgion的引用的卡片的索引,其卡片索引都存儲在CHeapBitMap結構里。偽代碼類似:hash_map 。
Snapshot-At-The-Beginning(SATB)
SATB是在G1 GC在并發(fā)標記階段使用的增量式的標記算法。并發(fā)標記是并發(fā)多線程的,但并發(fā)線程在同一時刻只掃描一個分區(qū)。在解釋SATB前先要了解三色標記法。三色標記法是將對象的存活狀態(tài)用三種顏色標記,從黑色到灰色逐層標記:
黑:該對象被標記了,并且其引用的對象也都被標記完成。
灰:對象被標記了,但其引用的對象還沒有被標記完。
白:對象還沒有被標記,標記階段結束后,會被回收。
在CMS GC中,并發(fā)標記階段使用的是Incremental update批量更新算法,在增加引用時的寫屏障中觸發(fā)新的對象引用的標記(三色標記法)。G1的并發(fā)標記算法,使用的是SATB。在GC開始時先創(chuàng)建一個對象快照,STAB可以在并發(fā)標記時標記所有快照中當時的存活對象。標記過程中新分配的對象也會被標記為存活對象,不會被回收。STAB核心的兩個結構就是兩個BitMap。如下:
// from G1ConcurrentMark-可以認為Bitmap的內部存儲著對象地址(reference 是8byte,所以Bitmap存儲著一個個64bit結構)
G1CMBitMap* _prev_mark_bitmap; // 全局的bitmap,存儲PTAMS偏移位置,也即當前標記的對象的地址(初始值是對應上次已經(jīng)標記完成的地址)
G1CMBitMap* _next_mark_bitmap; // 全局的bitmap,存儲NTAMS偏移位置。標記過程不斷移動,標記完成后會和prev_map 互換。
bitmap分別存儲著每個分區(qū)中,并發(fā)標記過程里的兩個重要的變量:PTAMS(pre-top-at-mark-start,代表著分區(qū)上一次完成標記的位置) 以及NTAMS(next-top-at-mark-start,隨著標記的進行會不斷移動,一開始在top位置)。SATB通過控制兩個變量的移動來進行標記。為了直觀了解標記過程,如下圖所示:

A:初始標記,因為要掃描所有Root Trace可達的對象,會有STW的暫停時間,會將掃描分區(qū)的NTAMS值設置為分區(qū)的頂部(Top)。B:最終標記,因為并發(fā)導致會有新分配的對象,因為并發(fā)標記過程中對象會被分配到NTAMS~TOP中間的區(qū)域。這些對象會被定義為”隱式對象“。因為NTAMS有很多值了,所以 next_mark_bitmap也會開始存儲NTAMS標記的對象的地址。C:清除階段:next_mark_bitmap和 prev_mark_bitmap會進行Swap。PTAMS和NTAMS也會互換值。清除所有Bottom~PTAMS的對象。對于”隱式對象“會在下次垃圾收集過程進行回收(如圖F過程)。這也是SATB存在弊端,會一定程度產(chǎn)生未能在本次標記中識別的浮動垃圾。
另,以上過程省略了根分區(qū)掃描和并發(fā)標記。上圖是包含了兩次標記過程,主要是為了展示B-E過程中,并發(fā)情況新對象的分配。
G1 GC的分類和過程 JDK10 之前的G1中的GC只有YoungGC,MixedGC。FullGC處理會交給單線程的Serial Old垃圾收集器。
YoungGC年輕代收集 在分配一般對象(非巨型對象)時,當所有eden region使用達到最大閥值并且無法申請足夠內存時,會觸發(fā)一次YoungGC。每次younggc會回收所有Eden以及Survivor區(qū),并且將存活對象復制到Old區(qū)以及另一部分的Survivor區(qū)。到Old區(qū)的標準就是在PLAB中得到的計算結果。因為YoungGC會進行根掃描,所以會stop the world。
YoungGC的回收過程如下:
1.根掃描,跟CMS類似,Stop the world,掃描GC Roots對象。
2.處理Dirty card,更新RSet.
3.掃描RSet,掃描RSet中所有old區(qū)對掃描到的young區(qū)或者survivor去的引用。
4.拷貝掃描出的存活的對象到survivor2/old區(qū)
5.處理引用隊列,軟引用,弱引用,虛引用(下一篇優(yōu)化中會再講一下這三種引用對gc的影響)
MixGC混合收集
MixedGC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。哪些old region能夠放到CSet里面,有很多參數(shù)可以控制。比如G1HeapWastePercent參數(shù),在一次younggc之后,可以允許的堆垃圾百占比,超過這個值就會觸發(fā)mixedGC。G1MixedGCLiveThresholdPercent參數(shù)控制的,old代分區(qū)中的存活對象比,達到閥值時,這個old分區(qū)會被放入CSet。源碼可以看下gc/g1/collectionSetChooser。
MixedGC一般會發(fā)生在一次YoungGC后面,為了提高效率,MixedGC會復用YoungGC的全局的根掃描結果,因為這個Stop the world過程是必須的,整體上來說縮短了暫停時間。
MixGC的回收過程可以理解為YoungGC后附加的全局concurrent marking,全局的并發(fā)標記主要用來處理old區(qū)(包含H區(qū))的存活對象標記,過程如下:
初始標記(InitingMark)。標記GC Roots,會STW,一般會復用YoungGC的暫停時間。如前文所述,初始標記會設置好所有分區(qū)的NTAMS值。
根分區(qū)掃描(RootRegionScan)。這個階段GC的線程可以和應用線程并發(fā)運行。其主要掃描初始標記以及之前YoungGC對象轉移到的Survivor分區(qū),并標記Survivor區(qū)中引用的對象。所以此階段的Survivor分區(qū)也叫根分區(qū)(RootRegion)。部分源碼如下:
// 當有需要掃描的的S分區(qū)時,該Task會被開啟,掃描后會執(zhí)行scan_finished,notify其他GC活動,如youngGC
class G1CMRootRegionScanTask : public AbstractGangTask {
G1ConcurrentMark* _cm;
public:
G1CMRootRegionScanTask(G1ConcurrentMark* cm) :
AbstractGangTask("G1 Root Region Scan"), _cm(cm) { }
void work(uint worker_id) {
assert(Thread::current()->is_ConcurrentGC_thread(),
"this should only be done by a conc GC thread");
G1CMRootRegions* root_regions = _cm->root_regions(); // _root_regions 初始化為待掃描的Survivor分區(qū)。
HeapRegion* hr = root_regions->claim_next();
while (hr != NULL) { // 循環(huán)分別處理所有待掃描的S分區(qū)
_cm->scan_root_region(hr, worker_id); //方法如下
hr = root_regions->claim_next();
}
}
};
// 掃描Survivor區(qū) (HeapRegion* hr)
void G1ConcurrentMark::scan_root_region(HeapRegion* hr, uint worker_id) {
assert(hr->next_top_at_mark_start() == hr->bottom(), "invariant");
G1RootRegionScanClosure cl(_g1h, this, worker_id);
const uintx interval = PrefetchScanIntervalInBytes;
HeapWord* curr = hr->bottom(); // 掃描分區(qū)的bottom
const HeapWord* end = hr->top(); // 掃描分區(qū)的top
while (curr < end) { // 掃描所有bottom到top的分區(qū)的對象
Prefetch::read(curr, interval);
oop obj = oop(curr);
int size = obj->oop_iterate_size(&cl);
assert(size == obj->size(), "sanity");
curr += size;
}
}
3.并發(fā)標記(ConcurrentMark)。會并發(fā)標記所有非完全空閑的分區(qū)的存活對象,也即使用了SATB算法,標記各個分區(qū)。
4.最終標記(Remark)。主要處理SATB緩沖區(qū),以及并發(fā)標記階段未標記到的漏網(wǎng)之魚(存活對象),會STW,可以參考上文的SATB處理。
5.清除階段(Clean UP)。上述SATB也提到了,會進行bitmap的swap,以及PTAMS,NTAMS互換。整理堆分區(qū),調整相應的RSet(比如如果其中記錄的Card中的對象都被回收,則這個卡片的也會從RSet中移除),如果識別到了完全空的分區(qū),則會清理這個分區(qū)的RSet。這個過程會STW。
清除階段之后,還會對存活對象進行轉移(復制算法),轉移到其他可用分區(qū),所以當前的分區(qū)就變成了新的可用分區(qū)。復制轉移主要是為了解決分區(qū)內的碎片問題。
FullGC
G1在對象復制/轉移失敗或者沒法分配足夠內存(比如巨型對象沒有足夠的連續(xù)分區(qū)分配)時,會觸發(fā)FullGC。FullGC使用的是stop the world的單線程的Serial Old模式,所以一旦觸發(fā)FullGC則會STW應用線程,并且執(zhí)行效率很慢。JDK 8版本的G1是不提供Full gc的處理的。對于G1 GC的優(yōu)化,很大的目標就是沒有FullGC。
上文內容都是基于JDK 8的版本的,在jdk10版本的G1 GC會有很多優(yōu)化。Full CG方面,將提供并發(fā)標記的Full GC方案:Parallelize Mark-Sweep-Compact。Card Table的掃描也會得到加速。RSet也優(yōu)化了,目前的RSet會存儲在所有的分區(qū)里,新版本的RSet只需要在CSet中,并且是在Remark到Clean階段之間并發(fā)構建RSet。這項優(yōu)化會增加整個并發(fā)標記的周期,但是縮減了很多RSet的占用空間。另外,對于PauseTime會有更精準的處理,在MixedGC的對象拷貝階段,提供了可放棄拷貝的(Abortable)選項。MixedGC會計算下一個Region的對象拷貝,如果可能會超過預期的pause time,則會放棄這次拷貝。
了解了G1垃圾收集器的運行機制之后,就可以針對一些GC相關參數(shù)來調整內存分配以及運行策略。下述的調優(yōu)主要針對G1垃圾收集器進行介紹,以及會分析一下G1 GC的日志格式。
G1 GC日志分析
在執(zhí)行具體的調優(yōu)任務前,需要結合GC日志以及應用本身的特點。打印詳細GClog,需要添加如下啟動參數(shù):
-XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps
本文使用的Java version:
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
下面截取gc.log 中的一次YoungGC和一次MixedGC。
2018-05-26T19:51:45.808-0800: 127.031: [GC pause (G1 Evacuation Pause) (young), 0.0063650 secs]
[Parallel Time: 5.5 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 127030.7, Avg: 127030.7, Max: 127030.7, Diff: 0.0]
[Ext Root Scanning (ms): Min: 1.1, Avg: 1.3, Max: 1.5, Diff: 0.4, Sum: 5.3]
[Update RS (ms): Min: 1.3, Avg: 1.4, Max: 1.4, Diff: 0.1, Sum: 5.4]
[Processed Buffers: Min: 3, Avg: 12.5, Max: 24, Diff: 21, Sum: 50]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.5, Sum: 1.3]
[Object Copy (ms): Min: 2.2, Avg: 2.4, Max: 2.5, Diff: 0.4, Sum: 9.5]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 5.4, Avg: 5.4, Max: 5.4, Diff: 0.1, Sum: 21.6]
[GC Worker End (ms): Min: 127036.1, Avg: 127036.1, Max: 127036.1, Diff: 0.0]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 39.0M(39.0M)->0.0B(2048.0K) Survivors: 3072.0K->4096.0K Heap: 111.4M(128.0M)->72.9M(128.0M)]
[Times: user=0.02 sys=0.00, real=0.01 secs]
2018-05-26T19:57:20.534-0800: 461.748: [GC pause (G1 Evacuation Pause) (mixed), 0.0685311 secs]
[Parallel Time: 67.2 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 461748.1, Avg: 461748.1, Max: 461748.1, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.8, Avg: 2.6, Max: 7.5, Diff: 6.6, Sum: 10.5]
[Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.7, Diff: 0.7, Sum: 1.4]
[Processed Buffers: Min: 0, Avg: 9.2, Max: 35, Diff: 35, Sum: 37]
[Scan RS (ms): Min: 29.7, Avg: 34.3, Max: 36.1, Diff: 6.5, Sum: 137.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.5, Max: 0.8, Diff: 0.8, Sum: 2.0]
[Object Copy (ms): Min: 28.8, Avg: 29.3, Max: 29.8, Diff: 1.0, Sum: 117.1]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 67.1, Avg: 67.1, Max: 67.1, Diff: 0.0, Sum: 268.5]
[GC Worker End (ms): Min: 461815.2, Avg: 461815.2, Max: 461815.2, Diff: 0.0]
[Code Root Fixup: 0.3 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 1.0 ms]
[Choose CSet: 0.4 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.2 ms]
[Eden: 5120.0K(5120.0K)->0.0B(57.0M) Survivors: 1024.0K->1024.0K Heap: 64.3M(128.0M)->55.8M(128.0M)]
[Times: user=0.07 sys=0.11, real=0.07 secs]
兩個收集過程的日志格式相似。先以young gc為例,分析一下日志信息。
GC并行任務
并行任務部分主要包含Parallel Time這一行以及其下面的詳細任務信息。
[Parallel Time: , GC Workers: ] [Parallel Time: 5.9 ms, GC Workers: 4] 這一行標記著并行階段的匯總信息。總共花費時間以及GC的工作線程數(shù)。后續(xù)的這兩行,start-end是時間戳信息。Diff是偏移平均時間的值。Diff越小越好,說明每個工作線程的速度都很均勻,如果Diff值偏大,就要看下面具體哪一項活動產(chǎn)生的波動。Avg代表平均時間值。如果Avg跟Min,Max偏差不大是比較正常的,否則也要詳細分析具體的偏差值大的任務。
[GC Worker Start (ms): Min: 127030.7, Avg: 127030.7, Max: 127030.7, Diff: 0.0]
[GC Worker End (ms): Min: 127036.1, Avg: 127036.1, Max: 127036.1, Diff: 0.0]
下一段顯示的是詳細的并行階段的GC活動。
[Ext Root Scanning (ms): Min: 1.1, Avg: 1.3, Max: 1.5, Diff: 0.4, Sum: 5.3]
[Update RS (ms): Min: 1.3, Avg: 1.4, Max: 1.4, Diff: 0.1, Sum: 5.4]
[Processed Buffers: Min: 3, Avg: 12.5, Max: 24, Diff: 21, Sum: 50]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.5, Sum: 1.3]
[Object Copy (ms): Min: 2.2, Avg: 2.4, Max: 2.5, Diff: 0.4, Sum: 9.5]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 5.4, Avg: 5.4, Max: 5.4, Diff: 0.1, Sum: 21.6]
Ext Root Scanning
外部根區(qū)掃描。外部根是堆外區(qū)。JNI引用,JVM系統(tǒng)目錄,Classloaders等。后面跟著具體的時間信息。
RSet的處理
log中RS指的是RSet。
UpdateRS:更新RSet的時間信息。-XX:MaxGCPauseMillis參數(shù)是限制G1的暫停之間,一般RSet更新的時間小于10%的目標暫停時間是比較可取的。如果花費在RSetUpdate的時間過長,可以修改其占用總暫停時間的百分比-XX:G1RSetUpdatingPauseTimePercent。這個參數(shù)的默認值是10。
Processed Buffers:已處理緩沖區(qū)。這個階段處理的是在優(yōu)化線程中處理dirty card分區(qū)掃描時記錄的日志緩沖區(qū)。
Scan RS:關于RSet的粒度,如果RSet中的Bitmap是粗粒度的,那么就會增加RSet掃描的時間。如下所示的掃描時間,說明還沒有粗化的RSet。
Code Root Scanning:代碼跟的掃描。只有在分區(qū)的RSet有強代碼根時會檢查CSet的對內引用,例如常量池。
[Update RS (ms): Min: 1.3, Avg: 1.4, Max: 1.4, Diff: 0.1, Sum: 5.4]
[Processed Buffers: Min: 3, Avg: 12.5, Max: 24, Diff: 21, Sum: 50]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.5, Sum: 1.3]
如果觀察到RS的處理時間較長,可以使用-XX:+G1SummarizeRSetStats參數(shù),在GC結束后打印RSet的詳細信息。一般在debug環(huán)境排查用。還有一個輔助參數(shù)G1SummarizeRSetStatsPeriod=0用來控制第幾次GC后統(tǒng)計一次RSet信息。
Object Copy
Object Copy:該任務主要是對CSet中存活對象進行轉移(復制)。對象拷貝的時間一般占用暫停時間的主要部分。如果拷貝時間和”預測暫停時間“有相差很大,也可以調整年輕代尺寸大小。
[Object Copy (ms): Min: 2.2, Avg: 2.4, Max: 2.5, Diff: 0.4, Sum: 9.5]
Termination
這里的終止主要是終止工作線程。Work線程在工作終止前會檢查其他工作線程的任務,如果其他work線程有沒完成的任務,會搶活。如果終止時間較長,額能是某個work線程在某項任務執(zhí)行時間過長。
GC Worker Other
花在GC之外的工作線程的時間,比如因為JVM的某個活動,導致GC線程被停掉。這部分消耗的時間不是真正花在GC上,只是作為log的一部分記錄。
GC Worker Total
并行階段的GC匯總,包含了GC以及GC Worker Other的總時間。
GC 串行活動
一下是串行的GC活動。包括代碼根的更新和掃描。Clear的時候還要清理RSet相應去除的Card Table信息。G1 GC在掃描Card信息時會有一個標記記錄,防止重復掃描同一個Card。
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
GC Other活動
剩余的部分就是其他GC活動了。主要包含:選擇CSet、引用處理和排隊、卡片重新臟化、回收空閑巨型分區(qū)以及在收集之后釋放CSet。
[Other: 0.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
Choose CSet:選擇CSet,因為年輕代的所有分區(qū)都會被收集,所以CSet不需要選擇,消耗時間都是0ms。Choose CSet任務一般都是在mixed gc的過程中觸發(fā)。
Ref Proc、Enq: 引用處理主要針對弱引用,軟引用,徐引用,final,JNI引用。將這些引用排列到相應的reference隊列中。
Redirty Cards:重新臟化卡片。排隊引用可能會更新RSet,所以需要對關聯(lián)的Card重新臟化(Redirty Cards)。
Humongous Register、Reclaim 主要是對巨型對象回收的信息,youngGC階段會對RSet中有引用的短命的巨型對象進行回收,巨型對象會直接回收而不需要進行轉移(轉移代價巨大,也沒必要)。
Free CSet: 釋放CSet,其中也會清理CSet中的RSet。
垃圾收集結果統(tǒng)計
如下對比了一次youngGC和一次mixedGC的垃圾收集結果:
young: [Eden: 39.0M(39.0M)->0.0B(2048.0K) Survivors: 3072.0K->4096.0K Heap: 111.4M(128.0M)->72.9M(128.0M)]
mixed: [Eden: 5120.0K(5120.0K)->0.0B(57.0M) Survivors: 1024.0K->1024.0K Heap: 64.3M(128.0M)->55.8M(128.0M)]
Eden: 39.0M(39.0M)->0.0B(2048.0K):Eden分區(qū)GC前39M,GC后是0。括號里面的分別是GC前后Eden分區(qū)的總大小。可以看到在一次GC后,Eden的空間做了調整。G1 GC的暫停時間是可預測的,所以YoungGC之后,會根據(jù)pause time的目標重新計算需要的Eden分區(qū)數(shù),進行動態(tài)調整。
Survivors: 3072.0K->4096.0K。Survivors空間的變化,空間增長了,說明有存活對象E區(qū)晉升到S區(qū)。
Heap: 111.4M(128.0M)->72.9M(128.0M)。整個堆區(qū)的GC前后空間數(shù)據(jù),G1 GC會動態(tài)調整堆區(qū),但這次回收中沒有改變堆區(qū)的容量。
G1 GC相關參數(shù)
G1 GC是垃圾收集優(yōu)先的垃圾收集器,同時有著”可預期的暫停時間“,垃圾收集過程是分代的,但堆空間是基于分區(qū)進行分配。所以整體的空間利用率,時間效率都有更大的提升。G1的YoungGC和MixedGC以及并發(fā)標記階段都有很多機制可以控制觸發(fā)時機,一般情況是不建議過度更改官方建議參數(shù)。但默認參數(shù)不一定適用于所有應用,調優(yōu)前需要有明確的目標,或者問題處理思路。
以下先整理下G1垃圾收集器可以調整的重要參數(shù):
-XX:+UseG1GC:啟用 G1 (Garbage First) 垃圾收集器
-XX:MaxGCPauseMillis:設置允許的最大GC停頓時間(GC pause time),這只是一個期望值,實際可能會超出,可以和年輕代大小調整一起并用來實現(xiàn)。默認是200ms。
-XX:G1HeapRegionSize:每個分區(qū)的大小,默認值是會根據(jù)整個堆區(qū)的大小計算出來,范圍是1M~32M,取值是2的冪,計算的傾向是盡量有2048個分區(qū)數(shù)。比如如果是2G的heap,那region=1M。16Gheap,region=8M。
-XX:MaxTenuringThreshold=n:晉升到老年代的“年齡”閥值,默認值為 15。
-XX:InitiatingHeapOccupancyPercent:一般會簡寫IHOP,默認是45%,這個占比跟并發(fā)周期的啟動相關,當空間占比達到這個值時,會啟動并發(fā)周期。如果經(jīng)常出現(xiàn)FullGC,可以調低該值,盡早的回收可以減少FullGC的觸發(fā),但如果過低,則并發(fā)階段會更加頻繁,降低應用的吞吐。
-XX:G1NewSizePercent:年輕代最小的堆空間占比,默認是5%。
-XX:G1MaxNewSizePercent:年輕代最大的堆空間占比,默認是60%。
-XX:ConcGCThreads:并發(fā)執(zhí)行的線程數(shù),默認值接近整個應用線程數(shù)的1/4。
-XX:-XX:G1HeapWastePercent:允許的浪費堆空間的占比,默認是5%。如果并發(fā)標記可回收的空間小于5%,則不會觸發(fā)MixedGC。
-XX:G1MixedGCCountTarget:一次全局并發(fā)標記之后,后續(xù)最多執(zhí)行的MixedGC次數(shù)。默認值是8.
G1 GC調優(yōu)建議
年輕代調優(yōu)
因為G1 GC是啟發(fā)式算法,會動態(tài)調整年輕代的空間大小。目標也就是為了達到接近預期的暫停時間。年輕代調優(yōu)中比較重要的就是對暫停時間的處理。一般都是根據(jù)MaxGCPauseMillis以及年輕代占比G1NewSizePercent、G1MaxNewSizePercent,結合應用的特點和GC數(shù)據(jù)進行接近期望pause time的調整。為了能觀察到詳細的暫停時間信息,可以添加調試的啟動參數(shù) -XX:+PrintAdaptiveSizePolicy。下面摘取一段youngGC gc log的輸出:
26.139: [GC pause (G1 Evacuation Pause) (young) 26.139: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 3484, predicted base time: 5.51 ms, remaining time: 194.49 ms, target pause time: 200.00 ms]
26.139: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 54 regions, survivors: 9 regions, predicted young region time: 5.98 ms]
26.139: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 54 regions, survivors: 9 regions, old: 0 regions, predicted pause time: 11.49 ms, target pause time: 200.00 ms]
, 0.0163685 secs]
target也即目標是200ms,實際的pause time是16ms。遠遠小于目標暫停時間。并且再CSet中的分區(qū)數(shù)是“eden: 54 regions, survivors: 9 regions”,可以適當增加CSet中的年輕代分區(qū),也可以適當縮短暫停時間,讓實際值和期望值不斷接近。
并發(fā)標記和MixGC 調優(yōu)
InitiatingHeapOccupancyPercent就是觸發(fā)并發(fā)標記的一個決定閥值。當Java堆空間占用到45%便開啟并發(fā)周期。并發(fā)標記的初始標記階段伴隨著一次YoungGC的暫停。會看到如下log記錄:
2018-05-26T19:50:57.256-0800: 78.480: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0076560 secs]
IHOP如果閥值設置過高,可能會遇到轉移失敗的風險,比如對象進行轉移時空間不足。如果閥值設置過低,就會使標記周期運行過于頻繁,并且有可能混合收集期回收不到空間。IHOP值如果設置合理,但是在并發(fā)周期時間過長時,可以嘗試增加并發(fā)線程數(shù),調高ConcGCThreads。
引用處理
G1 GC對于虛引用、弱引用、軟引用的處理會比一般對象多一些收集任務。如果在引用處理占用了很長時間,需要更進一步排查。在并發(fā)標記的Remark階段會記錄引用的處理,日志信息如下:
如下:
[GC remark 2018-05-26T19:50:57.386-0800: 78.610: [Finalize Marking, 0.0002675 secs] 2018-05-26T19:50:57.386-0800: 78.611: [GC ref-proc, 0.0001091 secs] 2018-05-26T19:50:57.386-0800: 78.611: [Unloading, 0.0204521 secs], 0.0212793 secs]
可以通過-XX:+PrintReferenceGC打印更詳細的引用計數(shù)信息:
[SoftReference, 0 refs, 0.0000482 secs]2018-06-03T20:52:03.887-0800: 18.033: [WeakReference, 116 refs, 0.0000321 secs]2018-06-03T20:52:03.887-0800: 18.033: [FinalReference, 1073 refs, 0.0009571 secs]2018-06-03T20:52:03.888-0800: 18.034: [PhantomReference, 0 refs, 1 refs, 0.0000211 secs]2018-06-03T20:52:03.888-0800: 18.034: [JNI Weak Reference, 0.0000192 secs], 0.0084976 secs]
一般在Ref Proc時間超過GC暫停時間的10%時就要關注。Ref Proc的信息打印在每次垃圾收集的Other信息模塊:
[Other: 0.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.4 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
如果SoftReference過多,會有頻繁的老年代收集。-XX:SoftRefLRUPolicyMSPerMB參數(shù),可以指定每兆堆空閑空間的軟引用的存活時間,默認值是1000,也就是1秒。可以調低這個參數(shù)來觸發(fā)更早的回收軟引用。如果調高的話會有更多的存活數(shù)據(jù),可能在GC后堆占用空間比會增加。對于軟引用,還是建議盡量少用,會增加存活數(shù)據(jù)量,增加GC的處理時間。
總結
本文簡單介紹了一下G1 GC的調優(yōu)參數(shù)以及G1 GC的日志內容。在具體調優(yōu)過程,可以增加一些調優(yōu)的參數(shù),如-XX:+G1SummarizeRSetStats、-XX:+PrintReferenceGC、-XX:+PrintAdaptiveSizePolicy等。每次調參后還要密切關注GC log,最好能模擬生產(chǎn)環(huán)境進行全鏈路的測試。沒有一個參數(shù)的調整是可以普適任何應用的,如果沒有GC問題,就不需要為了優(yōu)化而優(yōu)化。但是對于GC的知識儲備和更新,是每個應用開發(fā)工程師必備的知識模塊。

助力秋招-獨孤九劍蕩劍式 | Java語言&基礎面試題
JVM架構體系與GC命令小總結
Hive性能調優(yōu) | 并行執(zhí)行/嚴格模式/JVM重用/推測執(zhí)行

版權聲明:
編輯?《大數(shù)據(jù)技術與架構》
文章不錯?點個【在看】吧!??




