JVM性能调优实践:G1 垃圾收集器介绍篇
前言
前面兩篇主要整理了性能測試的主要觀察指標信息:性能測試篇,以及JVM性能調優的工具:JVM篇。這一篇先簡單總結一下GC的種類,然后側重總結下G1(Garbage-First)垃圾收集器的分代,結合open-jdk源碼分析下重要算法如SATB,重要存儲結構如CSet、RSet、TLAB、PLAB、Card Table等。最后會再梳理下G1 GC的YoungGC,MixedGC收集過程。
GC的分類
GC的主要回收區域就是年輕代(young gen)、老年代(tenured gen)、持久區(perm gen),在jdk8之后,perm gen消失,被替換成了元空間(Metaspace),元空間會在普通的堆區進行分配。垃圾收集為了提高效率,采用分代收集的方式,對于不同特點的回收區域使用不同的垃圾收集器。系統正常運行情況young是比較頻繁的,full gc會觸發整個heap的掃描和回收。在G1垃圾收集器中,最好的優化狀態就是通過不斷調整分區空間,避免進行full gc,可以大幅提高吞吐量。下面會詳細介紹。
串行垃圾回收器
JDK 1.3之前的垃圾回收器,單線程回收,并且會有stop theworld(下文會簡稱STW),也即GC時,暫停所有用戶線程。其運行方式是單線程的,適合Client模式的應用,適合單CPU環境。串行的垃圾收集器有兩種,Serial以及SerialOld,一般會搭配使用。新生代使用Serial采取復制算法,老年代使用Serial Old采取標記整理算法。Client應用或者命令行程序可以,通過-XX:+UseSerialGC可以開啟上述回收模式。
Serial:用于新生代垃圾收集,復制算法
SerialOld:用于老年代垃圾收集,標記整理算法
并行垃圾回收器
整體來說,并行垃圾回收相對于串行,是通過多線程運行垃圾收集的。也會stop-the-world。適合Server模式以及多CPU環境。一般會和jdk1.5之后出現的CMS搭配使用。并行的垃圾回收器有以下幾種:
ParNew:Serial收集器的多線程版本,默認開啟的收集線程數和cpu數量一樣,運行數量可以通過修改ParallelGCThreads設定。用于新生代收集,復制算法。使用-XX:+UseParNewGC,和Serial Old收集器組合進行內存回收。
Parallel Scavenge: 關注吞吐量,吞吐量優先,吞吐量=代碼運行時間/(代碼運行時間+垃圾收集時間),也就是高效率利用cpu時間,盡快完成程序的運算任務?
可以設置最大停頓時間MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。如果設置了-XX:+UseAdaptiveSizePolicy參數,則隨著GC,會動態調整新生代的大小,Eden,Survivor比例等,以提供最合適的停頓時間或者最大的吞吐量。用于新生代收集,復制算法。通過-XX:+UseParallelGC參數,Server模式下默認提供了其和SerialOld進行搭配的分代收集方式。
Parllel Old:Parallel Scavenge的老年代版本。JDK 1.6開始提供的。在此之前Parallel Scavenge的地位也很尷尬,而有了Parllel Old之后,通過-XX:+UseParallelOldGC參數使用Parallel Scavenge + Parallel Old器組合進行內存回收。
并發標記掃描垃圾回收器(CMS)
CMS(Concurrent Mark Sweep)基于“標記—清除”算法,用于老年代,所以其關注點在于減少“pause time”也即因垃圾回收導致的stop the world時間。對于重視服務的響應速度的應用可以使用CMS。因為CMS是“并發”運行的,也即垃圾收集線程可以和用戶線程同時運行。 缺點就是會產生內存碎片。?
CMS的回收分為幾個階段:
初始標記:標記一下GC Roots能直接關聯到的對象,會“Stop The World”
并發標記:GC Roots Tracing,可以和用戶線程并發執行。
重新標記:標記期間產生的對象存活的再次判斷,修正對這些對象的標記,執行時間相對并發標記短,會“Stop The World”。
并發清除:清除對象,可以和用戶線程并發執行。
CMS最主要解決了pause time,但是會占用CPU資源,犧牲吞吐量。CMS默認啟動的回收線程數是(CPU數量+3)/ 4,當CPU<4個時,會影響用戶線程的執行。另外一個缺點就是內存碎片的問題了,碎片會給大對象的內存分配造成麻煩,如果老年代的可用的連續空間也無法分配時,會觸發full gc。并且full gc時如果發生young gc會被young gc打斷,執行完young gc之后再繼續執行full gc。?
-XX:UseConcMarkSweepGC參數可以開啟CMS,年輕代使用ParNew,老年代使用CMS,同時Serial Old收集器將作為CMS收集器出現Concurrent Mode Failure失敗后的后備收集器使用。
G1垃圾收集器
G1(Garbage-First)是在JDK 7u4版本之后發布的垃圾收集器,并在jdk9中成為默認垃圾收集器。通過“-XX:+UseG1GC”啟動參數即可指定使用G1 GC。從整體來說,G1也是利用多CPU來縮短stop the world時間,并且是高效的并發垃圾收集器。但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因為G1中的垃圾收集區域是“分區”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年輕代的ygc,全堆掃描的fullgc外,還有包含所有年輕代以及部分老年代Region的MixedGC。G1的優勢還有可以通過調整參數,指定垃圾收集的最大允許pause time。下面會詳細闡述下G1分區以及分代的概念,以及G1 GC的幾種收集過程的分類。
G1分區的概念
在G1之前的垃圾收集器,將堆區主要劃分了Eden區,Old區,Survivor區。其中對于Eden,Survivor對回收過程來說叫做“年輕代垃圾收集”。并且年輕代和老年代都分別是連續的內存空間。?
G1將堆分成了若干Region,以下和”分區”代表同一概念。Region的大小可以通過G1HeapRegionSize參數進行設置,其必須是2的冪,范圍允許為1Mb到32Mb。 JVM的會基于堆內存的初始值和最大值的平均數計算分區的尺寸,平均的堆尺寸會分出約2000個Region。分區大小一旦設置,則啟動之后不會再變化。如下圖簡單畫了下G1分區模型。?
Eden regions(年輕代-Eden區)
Survivor regions(年輕代-Survivor區)
Old regions(老年代)
Humongous regions(巨型對象區域)
Free resgions(未分配區域,也會叫做可用分區)-上圖中空白的區域
關于分區有幾個重要的概念:
G1還是采用分代回收,但是不同的分代之間內存不一定是連續的,不同分代的Region的占用數也不一定是固定的(不建議通過相關選項顯式設置年輕代大小。會覆蓋暫停時間目標。)。年輕代的Eden,Survivor數量會隨著每一次GC發生相應的改變。
分區是不固定屬于哪個分代的,所以比如一次ygc過后,原來的Eden的分區就會變成空閑的可用分區,隨后也可能被用作分配巨型對象,成為H區等。
G1中的巨型對象是指,占用了Region容量的50%以上的一個對象。Humongous區,就專門用來存儲巨型對象。如果一個H區裝不下一個巨型對象,則會通過連續的若干H分區來存儲。因為巨型對象的轉移會影響GC效率,所以并發標記階段發現巨型對象不再存活時,會將其直接回收。ygc也會在某些情況下對巨型對象進行回收。
通過上圖可以看出,分區可以有效利用內存空間,因為收集整體是使用“標記-整理”,Region之間基于“復制”算法,GC后會將存活對象復制到可用分區(未分配的分區),所以不會產生空間碎片。
G1類似CMS,也會在比如一次fullgc中基于堆尺寸的計算重新調整(增加)堆的空間。但是相較于執行fullgc,G1 GC會在無法分配對象或者巨型對象無法獲得連續分區來分配空間時,優先嘗試擴展堆空間來獲得更多的可用分區。原則上就是G1會計算執行GC的時間,并且極力減少花在GC上的時間(包括ygc,mixgc),如果可能,會通過不斷擴展堆空間來滿足對象分配、轉移的需要。
因為G1提供了“可預測的暫停時間”,也是基于G1的啟發式算法,所以G1會估算年輕代需要多少分區,以及還有多少分區要被回收。ygc觸發的契機就是在Eden分區數量達到上限時。一次ygc會回收所有的Eden和survivor區。其中存活的對象會被轉移到另一個新的survivor區或者old區,如果轉移的目標分區滿了,會再將可用區標記成S或者O區。
G1 中的重要數據結構、算法
在提及G1的垃圾收集過程時,需要理解幾個G1的重要的分區內部的詳細數據結構、以及核心算法。
TLAB(Thread Local Allocation Buffer)本地線程緩沖區
G1 GC會默認會啟用Tlab優化。其作用就是在并發情況下,基于CAS的獨享線程(mutator threads)可以優先將對象分配在一塊內存區域(屬于Java堆的Eden中),只是因為是Java線程獨享的內存區,沒有鎖競爭,所以分配速度更快,每個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區沒有超出閥值,則會獲取新的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(卡片)數據
? ? dirty_young_block(result, *actual_word_size);
? } else {
? ? *actual_word_size = 0;
? }
? return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PLAB(Promotion Local Allocation Buffer) 晉升本地分配緩沖區
在ygc中,對象會將全部Eden區存貨的對象轉移(復制)到S區分區。也會存在S區對象晉升(Promotion)到老年代。這個決定晉升的閥值可以通過MaxTenuringThreshold設定。晉升的過程,無論是晉升到S還是O區,都是在GC線程的PLAB中進行。每個GC線程都有一個PLAB。
Collection Sets(CSets)待收集集合
GC中待回收的region的集合。CSet中可能存放著各個分代的Region。CSet中的存活對象會在gc中被移動(復制)。GC后CSet中的region會成為可用分區。
Card Table 卡表
將Java堆劃分為相等大小的一個個區域,這個小的區域(一般size在128-512字節)被當做Card,而Card Table維護著所有的Card。Card Table的結構是一個字節數組,Card Table用單字節的信息映射著一個Card。當Card中存儲了對象時,稱為這個Card被臟化了(dirty card)。 對于一些熱點Card會存放到Hot card cache。同Card Table一樣,Hot card cache也是全局的結構。
Remembered Sets(RSets)已記憶集合
已記憶集合在每個分區中都存在,并且每個分區只有一個RSet。其中存儲著其他分區中的對象對本分區對象的引用,是一種points-in結構。ygc的時候,只要掃描RSet中的其他old區對象對于本young區的引用,不需要掃描所有old區。mixed gc時,掃描Old區的RSet中,其他old區對于本old分區的引用,一樣不用掃描所有的old區。提高了GC效率。因為每次GC都會掃描所有young區對象,所以RSet只有在掃描old引用young,old引用old時會被使用。?
為了防止RSet溢出,對于一些比較“Hot”的RSet會通過存儲粒度級別來控制。RSet有三種粒度,對于“Hot”的RSet在存儲時,根據細粒度的存儲閥值,可能會采取粗粒度。?
這三種粒度的RSet都是通過PerRegionTable來維護內部數據的??梢圆榭雌洳糠衷创a如下:
class PerRegionTable: public CHeapObj<mtGC> {
? friend class OtherRegionsTable;
? friend class HeapRegionRemSetIterator;
? HeapRegion* ? ? _hr; // 來自其他分區的引用
? 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
簡要結構如下圖(圖片來源)?
下面是三種粒度級別,以及對應的簡要數據結構:
細粒度(fine),其PerRegionTable存儲了所有對于本Resgion的引用的卡片的索引,其卡片索引都存儲在CHeapBitMap結構里。偽代碼類似:hash_map
Snapshot-At-The-Beginning(SATB)
SATB是在G1 GC在并發標記階段使用的增量式的標記算法。并發標記是并發多線程的,但并發線程在同一時刻只掃描一個分區。?
在解釋SATB前先要了解三色標記法。三色標記法是將對象的存活狀態用三種顏色標記,從黑色到灰色逐層標記:
黑:該對象被標記了,并且其引用的對象也都被標記完成。
灰:對象被標記了,但其引用的對象還沒有被標記完。
白:對象還沒有被標記,標記階段結束后,會被回收。
在CMS GC中,并發標記階段使用的是Incremental update批量更新算法,在增加引用時的寫屏障中觸發新的對象引用的標記(三色標記法)。?
G1的并發標記算法,使用的是SATB。在GC開始時先創建一個對象快照,STAB可以在并發標記時標記所有快照中當時的存活對象。標記過程中新分配的對象也會被標記為存活對象,不會被回收。STAB核心的兩個結構就是兩個BitMap。如下:
?// from G1ConcurrentMark-可以認為Bitmap的內部存儲著對象地址(reference 是8byte,所以Bitmap存儲著一個個64bit結構)
?G1CMBitMap* ? ? ? ? ? ? _prev_mark_bitmap; // ?全局的bitmap,存儲PTAMS偏移位置,也即當前標記的對象的地址(初始值是對應上次已經標記完成的地址)
?G1CMBitMap* ? ? ? ? ? ? _next_mark_bitmap; // 全局的bitmap,存儲NTAMS偏移位置。標記過程不斷移動,標記完成后會和prev_map 互換。?
1
2
3
4
bitmap分別存儲著每個分區中,并發標記過程里的兩個重要的變量:PTAMS(pre-top-at-mark-start,代表著分區上一次完成標記的位置) 以及NTAMS(next-top-at-mark-start,隨著標記的進行會不斷移動,一開始在top位置)。SATB通過控制兩個變量的移動來進行標記。為了直觀了解標記過程,如下圖所示:(原圖論文)?
A:初始標記,因為要掃描所有Root Trace可達的對象,會有STW的暫停時間,會將掃描分區的NTAMS值設置為分區的頂部(Top)。?
B:最終標記,因為并發導致會有新分配的對象,因為并發標記過程中對象會被分配到NTAMS~TOP中間的區域。這些對象會被定義為”隱式對象“。因為NTAMS有很多值了,所以_next_mark_bitmap也會開始存儲NTAMS標記的對象的地址。?
C:清除階段:_next_mark_bitmap和_prev_mark_bitmap會進行Swap。PTAMS和NTAMS也會互換值。清除所有Bottom~PTAMS的對象。對于”隱式對象“會在下次垃圾收集過程進行回收(如圖F過程)。這也是SATB存在弊端,會一定程度產生未能在本次標記中識別的浮動垃圾。
另,以上過程省略了根分區掃描和并發標記。上圖是包含了兩次標記過程,主要是為了展示B-E過程中,并發情況新對象的分配。
G1 GC的分類和過程
JDK10 之前的G1中的GC只有YoungGC,MixedGC。FullGC處理會交給單線程的Serial Old垃圾收集器。
YoungGC年輕代收集
在分配一般對象(非巨型對象)時,當所有eden region使用達到最大閥值并且無法申請足夠內存時,會觸發一次YoungGC。每次younggc會回收所有Eden以及Survivor區,并且將存活對象復制到Old區以及另一部分的Survivor區。到Old區的標準就是在PLAB中得到的計算結果。因為YoungGC會進行根掃描,所以會stop the world。
YoungGC的回收過程如下:
根掃描,跟CMS類似,Stop the world,掃描GC Roots對象。
處理Dirty card,更新RSet.
掃描RSet,掃描RSet中所有old區對掃描到的young區或者survivor去的引用。
拷貝掃描出的存活的對象到survivor2/old區
處理引用隊列,軟引用,弱引用,虛引用(下一篇優化中會再講一下這三種引用對gc的影響)
MixGC混合收集
MixedGC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。哪些old region能夠放到CSet里面,有很多參數可以控制。比如G1HeapWastePercent參數,在一次younggc之后,可以允許的堆垃圾百占比,超過這個值就會觸發mixedGC。G1MixedGCLiveThresholdPercent參數控制的,old代分區中的存活對象比,達到閥值時,這個old分區會被放入CSet。源碼可以看下gc/g1/collectionSetChooser。?
MixedGC一般會發生在一次YoungGC后面,為了提高效率,MixedGC會復用YoungGC的全局的根掃描結果,因為這個Stop the world過程是必須的,整體上來說縮短了暫停時間。
MixGC的回收過程可以理解為YoungGC后附加的全局concurrent marking,全局的并發標記主要用來處理old區(包含H區)的存活對象標記,過程如下:
1. 初始標記(InitingMark)。標記GC Roots,會STW,一般會復用YoungGC的暫停時間。如前文所述,初始標記會設置好所有分區的NTAMS值。?
2. 根分區掃描(RootRegionScan)。這個階段GC的線程可以和應用線程并發運行。其主要掃描初始標記以及之前YoungGC對象轉移到的Survivor分區,并標記Survivor區中引用的對象。所以此階段的Survivor分區也叫根分區(RootRegion)。部分源碼如下:
// 當有需要掃描的的S分區時,該Task會被開啟,掃描后會執行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分區。 ??
? ? HeapRegion* hr = root_regions->claim_next();
? ? while (hr != NULL) { // 循環分別處理所有待掃描的S分區 ??
? ? ? _cm->scan_root_region(hr, worker_id); ?//方法如下
? ? ? hr = root_regions->claim_next();
? ? }
? }
};
? // 掃描Survivor區 (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(); ? // 掃描分區的bottom
? const HeapWord* end = hr->top(); // 掃描分區的top
? while (curr < end) { // 掃描所有bottom到top的分區的對象 ?
? ? Prefetch::read(curr, interval);
? ? oop obj = oop(curr);
? ? int size = obj->oop_iterate_size(&cl);
? ? assert(size == obj->size(), "sanity");
? ? curr += size;
? }
} ??
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
3. 并發標記(ConcurrentMark)。會并發標記所有非完全空閑的分區的存活對象,也即使用了SATB算法,標記各個分區。?
4. 最終標記(Remark)。主要處理SATB緩沖區,以及并發標記階段未標記到的漏網之魚(存活對象),會STW,可以參考上文的SATB處理。?
5. 清除階段(Clean UP)。上述SATB也提到了,會進行bitmap的swap,以及PTAMS,NTAMS互換。整理堆分區,調整相應的RSet(比如如果其中記錄的Card中的對象都被回收,則這個卡片的也會從RSet中移除),如果識別到了完全空的分區,則會清理這個分區的RSet。這個過程會STW。
清除階段之后,還會對存活對象進行轉移(復制算法),轉移到其他可用分區,所以當前的分區就變成了新的可用分區。復制轉移主要是為了解決分區內的碎片問題。
FullGC
G1在對象復制/轉移失敗或者沒法分配足夠內存(比如巨型對象沒有足夠的連續分區分配)時,會觸發FullGC。FullGC使用的是stop the world的單線程的Serial Old模式,所以一旦觸發FullGC則會STW應用線程,并且執行效率很慢。JDK 8版本的G1是不提供Full gc的處理的。對于G1 GC的優化,很大的目標就是沒有FullGC。
總結
文章對目前JVM的幾種垃圾收集器做了簡單總結。詳細梳理了一下G1 GC的關鍵概念。本來想一起把G1 GC參數優化和GC Log也加上的,但篇幅有點長了,下一篇會加上[TODO->G1 垃圾收集器性能調優篇]。?
本文內容都是基于JDK 8的版本的,在jdk10版本的G1 GC會有很多優化。Full CG方面,將提供并發標記的Full GC方案:Parallelize Mark-Sweep-Compact。Card Table的掃描也會得到加速。RSet也優化了,目前的RSet會存儲在所有的分區里,新版本的RSet只需要在CSet中,并且是在Remark到Clean階段之間并發構建RSet。這項優化會增加整個并發標記的周期,但是縮減了很多RSet的占用空間。另外,對于PauseTime會有更精準的處理,在MixedGC的對象拷貝階段,提供了可放棄拷貝的(Abortable)選項。MixedGC會計算下一個Region的對象拷貝,如果可能會超過預期的pause time,則會放棄這次拷貝。對于JDK10的G1 GC更多信息可以看一下2018-Oracle G1 GC。
參考文獻
Garbage-First Garbage Collection?
introduction-g1-garbage-collector?
tuning-tips-G1-GC?
2018-Oracle G1 GC
---------------------?
作者:珠寶壹佰.未聞?
來源:CSDN?
原文:https://blog.csdn.net/lijingyao8206/article/details/80513383?
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
總結
以上是生活随笔為你收集整理的JVM性能调优实践:G1 垃圾收集器介绍篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 腹可安_功效作用注意事项用药禁忌用法用量
- 下一篇: Kafka设计原理看了又忘,忘了又看?