一.G1 GC術語Overview
1.1 並發
並發的意思是Java應用執行和垃圾收集活動可以同時進行
1.2 並行
並行的意思是垃圾收集運算是多線程執行的,比如CMS垃圾收集器的年輕代就是並行的,並行與串行的區別如下圖,左邊為串行,右邊為並行:

1.3 STW
STW(stop the world)意思是在一個垃圾回收事件中,所有Java應用線程會被暫停。只有暫停,應用才不會產生新的垃圾,有益於垃圾收集器更好的標記垃圾對象。(這就像是你在家掃狗毛,肯定要把狗先關籠子,停止它的活動)
1.4 Region
請先忘記這個圖,學習G1過程中不會對每個代進行設置了

G1垃圾收集器利用分而治之的思想將堆進行分區,划分為一個個的區域。每次收集的時候,只收集其中幾個區域,以此來控制垃圾回收產生的STW
G1和其他GC算法最大的區別是弱化分代概念,引入分區思想!!!

如果要另外選擇分區的尺寸,可以通過命令行選項:-XX:G1HeapRegionSize=n中進行設置
1.5 RSet
G1垃圾收集器里每一個RSet對應的是一個Region內部對象引用情況,說白了就是存在Region中存活對象的指針。在標記存活對象的時候,G1使用RSet概念,將每個分區指向分區內的引用記錄在該分區,避免對整個堆掃描,並行獨立處理垃圾集合
- 老年代對年輕代的引用,維護老年代分區指向年輕代分區的指針
- 老年代對老年代的引用。在這里,老年代中不同分區的指針將被維護在老年代擁有分區的RSet中
如下圖,我們可以看到3個分區,x(年輕代分區)、y和z(老年代分區)。x有一個來自z的對內引用。這個引用記錄在x的RSet中,分區z有2個對內引用,一個來自x一個來自y,因為年輕代分區作為一個整體回收的,所以只需記錄來自y的對內引用,不用記錄x的對內引用

1.6 CSet
Collection Set,簡稱CSet。在垃圾收集過程中收集的Region集合可以稱為收集集合(CSet),也就是在垃圾收集暫停過程中被回收的目標。GC時在CSet中的所有存活數據都會被轉移,分區釋放回空閑分區隊列
見下圖,左邊的年輕代收集CSet代表年輕代的一部分分區,右邊的混合收集CSet代表年輕代的一部分區和老年代的多個分區:

1.7 PLAB
Promotion Local Allocation Buffers,對象晉升到survivor分區或者老年代分區的過程是在GC線程的晉升本地分配緩沖區(PLAB)進行的,每個線程有獨立的PLAB。作用是避免多線程競爭相同數據。和下面介紹的TLAB思想是一致的
1.8 TLAB
Thread Local Allocation Buffers,線程本地分配緩存。JVM使用了TLAB這種線程專屬的區間來避免多線程沖突(無鎖方式),提高對象分配效率。TLAB本身占用了Eden空間,即JVM會為每一個線程都分配一塊TLAB空間

1.9 IHOP
InitiatingHeapOccupancyPercent,簡稱IHOP。缺省情況是Java堆內存的45%。當老年代的空間超過45%,G1會啟動一次混合周期收集
這也是G1和CMS之間較大的區別,G1的百分比是相對於整個Java堆而言的,CMS(CMSInitiatingOccupancyFraction)僅僅是針對老年代空間的占比。
為什么G1如此設計???
因為G1沒有固定物理上分割一塊內存作為老年代,而是用了Region的思想,這些Region可能是eden,survivor、老年代或者巨型分區,所以獲取針對老年代本身的占用百分比沒有意義
2.10 巨型分區
巨型對象會以連續分區的形式來存放,這種就叫巨型分區。巨型對象無法利用年輕代里的TLAB和PLAB。在JDK 8u40之前,它只能在並發收集周期的清除階段回收,但是在JDK 8u40之后,巨型分區可以在年輕代收集中和full GC被回收
二.G1的設計
2.1 為什么會有G1?
為什么會有G1呢?因為並發、並行和CMS垃圾收集器都有2個共同的問題:
- 老年代收集器大部分操作都必須掃描整個老年代空間(標記,清除和壓縮)。這就導致了GC隨着Java堆空間而線性增加或減少
- 年輕代和老年代是獨立的連續內存塊,所以要先決定年輕代和年老代放在虛擬地址空間的位置
2.2 Region的設計
上面說到,G1垃圾收集器利用分而治之的思想將堆進行分區,划分為一個個的區域。G1垃圾收集器將堆拆成一系列的分區,這樣的話,大部分的垃圾收集操作就只在一個分區內執行,而不是整個堆或者整個代

2.3 設計目標
G1的設計目標就是把必要的調整限定在以下2個:
- 設置最大的Java堆空間
- 設置指定GC暫停時間
G1會通過調整Java堆尺寸大小來滿足設定的暫停時間目標,暫停時間目標越短,年輕代空間越小,老年代空間相對越大
2.4 使用場景
G1 GC切分堆內存為多個區間(Region),從而避免很多GC操作在整個Java堆或者整個年輕代進行。G1 GC只關注你有沒有存貨對象,都會被回收並放入可用的Region隊列。G1 GC是基於Region的GC,適用於大內存機器。即使內存很大,Region掃描,性能還是很高的
如果現在采用的收集器沒有問題,就不要選擇G1,如果追求低停頓,那么G1已經是一個可嘗試的選擇,如果追求吞吐量,就不要選G1了
四.G1的垃圾回收
G1的垃圾收集周期主要有4種類型:年輕代收集周期、多級並發標記周期、混合收集周期和full GC(轉移失敗的安全保護機制)
這一節我會以應用啟動的時間順序來講,這樣比較易懂一點,也可以參照G1垃圾收集活動時序圖:

4.1 年輕代收集
應用剛啟動,慢慢流量進來,開始生成對象。G1會選一個分區並指定他為eden分區,當這塊分區用滿了之后,G1會選一個新的分區作為eden分區,這個操作會一直進行下去直到達到eden分區上限,也就是說eden分區已經被占滿,那么會觸發一次年輕代收集
年輕代收集首先做的就是遷移存活對象,它使用單eden,雙survivor進行復制算法,它將存活的對象從eden分區轉移到survivor分區,survivor分區內的某些對象達到了任期閾值之后,會晉升到老年代分區中。原有的年輕代分區會被整個回收掉
同時,年輕代收集還負責維護對象年齡,存活對象經歷過年輕代收集總次數等信息。G1將晉升對象的尺寸總和和它們的年齡信息維護到年齡表中,結合年齡表、survivor占比(--XX:TargetSurvivorRatio 缺省50%)、最大任期閾值(--XX:MaxTenuringThreshold 缺省為15)來計算出一個合適的任期閾值
調優:我們可以通過--XX:MaxGCPauseMillis,調優年輕代收集,縮小暫停時間
4.2 並發標記周期
隨着時間推移,越來越多的對象晉升到老年代中,當老年代占比(相對於Java總堆而言)達到IHOP參數(上圖的IHOP Trigger)之后,那么G1首先會觸發並發標記周期(上圖的Concurrent Marking Cycle),當完成后才會開始下一小節的混合垃圾收集周期
G1的並發標記循環分5個階段:
第一階段:初始標記(上圖Young Collection with Initial Mark),收集所有GC根(對象的起源指針,根引用),STW,在年輕代完成
第二階段:根區間掃描,標記所有幸存者區間的對象引用
第三階段:並發標記(上圖Concurrent Marking),標記存活對象
第四階段:重新標記(上圖Remark),是最后一個標記階段,STW,很短,完成所有標記工作
第五階段:清除(上圖Clean),回收沒有存活對象的Region並加入可用Region隊列
調優:我們可以通過--XX:InitiatingHeapOccupancyPercent,配置適合應用的IHOP值(過大會可能轉移失敗,過小可能過早引起並發標記周期)
我們也可以通過--XX:ConcGCThreads,增加並發線程數
4.3 混合收集周期
當達到IHOP參數並完成上一小節的並發標記周期之后,混合收集周期就啟動了,一個周期里的單次STW的混合收集和年輕代收集是類似的,唯一區別就是在混合收集過程中會包含一部分老年分區,所以也叫混合收集
看上圖的Mixed Collection Cycle,中間有好幾段Mixed Collection,說明混合收集周期包含多次收集次數。那么什么影響收集次數呢?是固定的?還是?有兩個參數比較重要:
-XX:G1MixedGCCountTarget:缺省值為8,意思是能啟動混合收集的數目設定一個物理限制。G1根據將回收的老年分區除以該參數值得到每次混合收集的老年代CSet最小數量
-XX:G1HeapWastePercent:缺省值為5%,每次混合收集暫停,G1算出廢物百分比,根據堆廢物百分比,當收集達到參數時,不再啟動新的混合收集
調優:當暫停時間和運行時間呈現指數級增長,可以通過-XX:G1HeapWastePercent,調高該參數會有所幫助,但這也導致更多碎片化
4.4 full GC
有2個條件同時滿足則會觸發full GC
1.拷貝存活對象晉升(promotion)失敗,無法找到可用的空閑分區,GC日志記錄為to-space exhausted。或者分配巨型對象無法在老年代找到連續足夠的分區

2.當發生第一個條件后,G1會嘗試增加堆使用量,如果擴展失敗,那么會觸發安全措施機制同時發生full GC
full GC中,單個線程會對整個堆的所有代中所有分區做標記、清除以及壓縮動作!!非常非常昂貴的操作!
