垃圾收集器之G1

G1垃圾收集器是一種工作在堆內不同分區上的併發收集器。分區既可以歸屬於老年代,也可以歸屬新生代,同一個代的分區不需要保持連續。爲老年代設計分區的初衷是我們發現併發後臺線程在回收老年代中沒有引用的對象時,有的分區垃圾對象的數量很多,另一些分區垃圾對象相對較少。

雖然分區的垃圾收集工作實際還是要暫停應用線程,不過由於G1收集器專注於垃圾最多的分區,最終的效果是花費較少的時間就能回收這些分區的垃圾。這種只專注於垃圾最多的分區的方式就是G1垃圾收集器的名稱由來,即首先收集垃圾最多的分區。

這一算法並不適用新生代的分區,新生代進行垃圾回收時,整個新生代空間要麼被回收,要麼被晉升。那麼新生代也採用分區的原因是因爲:採用預定義的分區能夠便於代的大小調整。

G1收集器的收集活動包括4種操作:

  • 新生代垃圾收集;
  • 後臺收集,併發週期;
  • 混合式垃圾收集;
  • 以及必要時的Full GC。

1、新生代垃圾收集

先看G1對新生代收集的前後對比,圖中的每個小方塊都代表一個G1的分區。分區中的黑色的區域代表數據,每個分區中的子母代表該區域屬於哪個代(E代表Eden,O代表老年代,S代表Survivor)。空的分區不屬於任何一個代;需要的時候G1收集器會強制指定這些空間的分區用於任何需要的代。

在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認爲這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC。

PS:在java 8中,持久代也移動到了普通的堆內存空間中,改爲元空間。

二、對象分配策略

說起大對象的分配,我們不得不談談對象的分配策略。它分爲3個階段:

  1. TLAB(Thread Local Allocation Buffer)線程本地分配緩衝區
  2. Eden區中分配
  3. Humongous區分配

TLAB爲線程本地分配緩衝區,它的目的爲了使對象儘可能快的分配出來。如果對象在一個共享的空間中分配,我們需要採用一些同步機制來管理這些空間內的空閒空間指針。在Eden空間中,每一個線程都有一個固定的分區用於分配對象,即一個TLAB。分配對象時,線程之間不再需要進行任何的同步。

對TLAB空間中無法分配的對象,JVM會嘗試在Eden空間中進行分配。如果Eden空間無法容納該對象,就只能在老年代中進行分配空間。

最後,G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式。

三、G1提供了兩種GC模式

3.1、G1 Young GC

Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據爲空,GC停止工作,應用線程繼續執行。

這時,我們需要考慮一個問題,如果僅僅GC 新生代對象,我們如何找到所有的根對象呢? 老年代的所有對象都是根麼?那這樣掃描下來會耗費大量的時間。於是,G1引進了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個heap區內的對象引用。

在CMS中,也有RSet的概念,在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out,在進行Young GC時,掃描根時,僅僅需要掃描這一塊區域,而不需要掃描整個老年代。

但在G1中,並沒有使用point-out,這是由於一個分區太小,分區數量太多,如果是用point-out的話,會造成大量的掃描浪費,有些根本不需要GC的分區引用也掃描了。於是G1中使用point-in來解決。point-in的意思是哪些分區引用了當前分區中的對象。這樣,僅僅將這些對象當做根來掃描就避免了無效的掃描。由於新生代有多個,那麼我們需要在新生代之間記錄引用嗎?這是不必要的,原因在於每次GC時,所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。

需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,爲了解決賦值器開銷這個問題,在G1 中又引入了另外一個概念,卡表(Card Table)。一個Card Table將一個分區在邏輯上劃分爲固定大小的連續區域,每個區域稱之爲卡。卡通常較小,介於128到512字節之間。Card Table通常爲字節數組,由Card的索引(即數組下標)來標識每個分區的空間地址。默認情況下,每個卡都未被引用。當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記爲”0″,即標記爲髒被引用,此外RSet也將這個數組下標記錄下來。一般情況下,這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。

Young GC 階段:

  • 階段1:根掃描
    靜態和本地對象被掃描
  • 階段2:更新RS
    處理dirty card隊列更新RS
  • 階段3:處理RS
    檢測從年輕代指向年老代的對象
  • 階段4:對象拷貝
    拷貝存活的對象到survivor/old區域
  • 階段5:處理引用隊列
    軟引用,弱引用,虛引用處理

3.2、G1 Mix GC

Mix GC不僅進行正常的新生代垃圾收集,同時也回收部分後臺掃描線程標記的老年代分區。

它的GC步驟分2步:

  1. 全局併發標記(global concurrent marking)
  2. 拷貝存活對象(evacuation)

在進行Mix GC之前,會先進行global concurrent marking(全局併發標記)。 global concurrent marking的執行過程是怎樣的呢?

在G1 GC中,它主要是爲Mixed GC提供標記服務的,並不是一次GC過程的一個必須環節。global concurrent marking的執行過程分爲五個步驟:

  • 初始標記(initial mark,STW)(第一次暫停所以應用線程)
    在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) 年輕代垃圾回收密切相關。
  • 根區域掃描(root region scan)
    G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,並且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。
  • 併發標記(Concurrent Marking)
    G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷
  • 最終標記(Remark,STW)(第二次暫停所以應用線程)
    該階段是 STW 回收,幫助完成標記週期。G1 GC 清空 SATB 緩衝區,跟蹤未被訪問的存活對象,並執行引用處理。
  • 清除垃圾(Cleanup,STW)(第三次暫停所以應用線程)
    在這個最後階段,G1 GC 執行統計和 RSet 淨化的 STW 操作。在統計期間,G1 GC 會識別完全空閒的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閒列表時爲部分併發。

四、三色標記算法

提到併發標記,我們不得不瞭解併發標記的三色標記算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。

  • 黑色:根對象,或者該對象與它的子對象都被掃描
  • 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
  • 白色:未被掃描對象,掃描完成所有對象之後,最終爲白色的爲不可達對象,即垃圾對象

當GC開始掃描對象時,按照如下圖步驟進行對象的掃描:

根對象被置爲黑色,子對象被置爲灰色。

 

繼續由灰色遍歷,將已掃描了子對象的對象置爲黑色。

遍歷了所有可達的對象後,所有可達的對象都變成了黑色。不可達的對象即爲白色,需要被清理。

這看起來很美好,但是如果在標記過程中,應用程序也在運行,那麼對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題

我們看下面一種情況,當垃圾收集器掃描到下面情況時:

這時候應用程序執行了以下操作:

A.c=C
B.c=null

這樣,對象的狀態圖變成如下情形:

這時候垃圾收集器再標記掃描的時候就會下圖成這樣:

很顯然,此時C是白色,被認爲是垃圾需要清理掉,顯然這是不合理的。那麼我們如何保證應用程序在運行的時候,GC標記的對象不丟失呢?有如下2中可行的方式:

  1. 在插入的時候記錄對象
  2. 在刪除的時候記錄對象

剛好這對應CMS和G1的2種不同實現方式:

在CMS採用的是增量更新(Incremental update),只要在寫屏障(write barrier)裏發現要有一個白對象的引用被賦值到一個黑對象 的字段裏,那就把這個白對象變成灰色的。即插入的時候記錄下來。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象,它有3個步驟:

1,在開始標記的時候生成一個快照圖標記存活對象

2,在併發標記的時候所有被改變的對象入隊(在write barrier裏把所有舊的引用所指向的對象都變成非白的)

3,可能存在遊離的垃圾,將在下次被收集

這樣,G1到現在可以知道哪些老的分區可回收垃圾最多。 當全局併發標記完成後,在某個時刻,就開始了Mix GC。這些垃圾回收被稱作“混合式”是因爲他們不僅僅進行正常的新生代垃圾收集,同時也回收部分後臺掃描線程標記的分區。混合式垃圾收集如下圖:

混合式GC也是採用的複製的清理策略,當GC完成後,會重新釋放空間。

至此,混合式GC告一段落了。下一小節我們講進入調優實踐。

五、調優實踐

五、調優實踐

5.1、4種情況會觸發這類的Full GC

G1收集器同CMS收集器一樣,在某些情況下,G1觸發了Full GC,這時G1會退化使用Serial收集器來完成垃圾的清理工作,它僅僅使用單線程來完成GC工作,GC暫停時間將達到秒級別的。整個應用處於假死狀態,不能處理任何請求,我們的程序當然不希望看到這些。有的時候你會在垃圾回收日誌中觀察到Full GC,這些日誌是一個信號,表明我們需要進一步調優(方式很多,甚至很可能要分配更多的堆空間)才能提升應用程序的性能。主要有4種情況會觸發這類的Full GC,如下:

1、併發模式失效

G1啓動標記週期,但在Mix GC之前,老年代就被填滿,這時候G1會放棄標記週期。這種情形下,需要增加堆大小,或者調整週期(例如增加線程數-XX:ConcGCThreads等)。

GC日誌如下的示例:

解決辦法:發生這種失敗意味着堆的大小應該增加了,或者G1收集器的後臺處理應該更早開始,或者需要調整週期,讓它運行得更快(如,增加後臺處理的線程數)。

2、晉升失敗

(to-space exhausted或者to-space overflow)

G1收集器完成了標記階段,開始啓動混合式垃圾回收,清理老年代的分區,不過,老年代空間在垃圾回收釋放出足夠內存之前就會被耗盡。(G1在進行GC的時候沒有足夠的內存供存活對象或晉升對象使用),由此觸發了Full GC。

下面日誌中(可以在日誌中看到(to-space exhausted)或者(to-space overflow)),反應的現象是混合式GC之後緊接着一次Full GC。

這種失敗通常意味着混合式收集需要更迅速的完成垃圾收集:每次新生代垃圾收集需要處理更多老年代的分區。

解決這種問題的方式是:

  1. 增加 -XX:G1ReservePercent 選項的值(並相應增加總的堆大小),爲“目標空間”增加預留內存量。
  2. 通過減少 -XX:InitiatingHeapOccupancyPercent 提前啓動標記週期。
  3. 也可以通過增加 -XX:ConcGCThreads 選項的值來增加並行標記線程的數目。

3、疏散失敗

(to-space exhausted或者to-space overflow)

進行新生代垃圾收集是,Survivor空間和老年代中沒有足夠的空間容納所有的倖存對象。這種情形在GC日誌中通常是:

這條日誌表明堆已經幾乎完全用盡或者碎片化了。G1收集器會嘗試修復這一失敗,但可以預期,結果會更加惡化:G1收集器會轉而使用Full GC。

解決這種問題的方式是:

  1. 增加 -XX:G1ReservePercent 選項的值(並相應增加總的堆大小),爲“目標空間”增加預留內存量。
  2. 通過減少 -XX:InitiatingHeapOccupancyPercent 提前啓動標記週期。
  3. 也可以通過增加 -XX:ConcGCThreads 選項的值來增加並行標記線程的數目。

4、巨型對象分配失敗

當巨型對象找不到合適的空間進行分配時,就會啓動Full GC,來釋放空間。這種情況下,應該避免分配大量的巨型對象,增加內存或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。

5.2、G1垃圾收集器調優

1、G1垃圾收集器調優的主要目標是避免發生併發模式失敗或者疏散失敗,一旦發生這些失敗就會導致Full GC。避免Full GC的技巧也適用於頻繁發生的新生代垃圾收集,這些垃圾收集需要等待掃描根分區完成才能進行。

2、其次,調優可以是過程中的停頓時間最小化。

下面列出能夠避免發生Full GC的方法:

  • 通過增加總的堆空間大小夥子調整老年代、新生代之間的比例來增加老年代空間的大小。
  • 增加後臺線程的數碼(假設我們有足夠的CPU資源運行這些線程)。
  • 以更高的頻率進行G1的後臺垃圾收集活動。
  • 在混合式垃圾收集週期中完成更多的垃圾收集工作。

使用G1垃圾收集器時,XX:MaxGCPauseMillis標誌有一個默認值:200毫秒(和throughput收集器有所不同)。如果G1收集器發生時空停頓(stop-the-world)的時長超過該值,G1收集器就會嘗試各種方式進行彌補--如調整新生代與老年代的比率,調整堆大小,更早地啓動後臺處理,改變晉升閾值,或者是在混合式垃圾收集週期中處理更多或者更少的老年代分區。

通常的取捨就是發生在這裏:如果減少參數值,爲了達到停頓時間的目標,新生代的大小會相應減少,不過新生代垃圾收集的頻率會更加頻繁。除此之外,爲了達到停頓時間的目標,混合式GC收集老年代分區數也會減少,而這會增大併發模式失敗發生的機會。

 

1、調整G1垃圾收集的後臺線程數

爲了讓G1贏得這場垃圾收集的比賽,可以嘗試增加後臺標記線程數碼(假如有足夠多的空閒CPU)。

調整方法:(與CMS類似),對於應用線程暫停運行的週期,可以使用ParallelGCThreads標誌設置運行的線程數;對於併發階段可以使用ConcGCThreads標誌設置運行線程數(注意此處的ConcGCThreads默認值不同CMS)。

2、調整G1垃圾收集器的運行頻率

如果G1更早的啓動垃圾收集,也能贏得比賽。G1週期通常在堆的佔用達到某個比率(通過參數:XX:InitiatingHeapOccupancyPercent=45設定),跟CMS不太一樣,這個參數值依據的是整個堆的使用情況而不是老年代的。

3、調整G1收集器的混合式垃圾收集週期

併發週期之後,老年代的標記分區回收完成之前,G1收集器無法啓動新的併發週期。因此,讓G1更早啓動標記週期的另一個方法是在混合式垃圾回收週期中儘量處理更多分區(如此一來最終的混合式GC週期就變少了)。

混合式垃圾收集處理工作量取決3個因素:

A、有多少分區被發現大部分是垃圾對象。如果分區的垃圾佔用達到35%,這個分區就被標記爲可以進行垃圾回收;(-XX:G1MixedGCLiveThresholdPercent=65)

B、G1回收分區時最大混合式GC週期數,可以通過參數-XX:G1MixedGCCountTarget=8

5.3、常見調優參數

-XX:MaxGCPauseMillis=N,(默認200毫秒,與throughput收集器有所不同)

前面介紹過使用GC的最基本的參數:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

前面2個參數都好理解,後面這個MaxGCPauseMillis參數該怎麼配置呢?這個參數從字面的意思上看,就是允許的GC最大的暫停時間。G1儘量確保每次GC暫停的時間都在設置的MaxGCPauseMillis範圍內。 那G1是如何做到最大暫停時間的呢?這涉及到另一個概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區域集合。

  • Young GC:選定所有新生代裏的region。通過控制新生代的region個數來控制young GC的開銷。
  • Mixed GC:選定所有新生代裏的region,外加根據global concurrent marking統計得出收集收益高的若干老年代region。在用戶指定的開銷目標範圍內儘可能選擇收益高的老年代region。

在理解了這些後,我們再設置最大暫停時間就好辦了。 首先,我們能容忍的最大暫停時間是有一個限度的,我們需要在這個限度範圍內設置。但是應該設置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個平衡。如果MaxGCPauseMillis設置的過小,那麼GC就會頻繁,吞吐量就會下降。如果MaxGCPauseMillis設置的過大,應用程序暫停時間就會變長。G1的默認暫停時間是200毫秒,我們可以從這裏入手,調整合適的時間。

5.4、其他調優參數

-XX:G1HeapRegionSize=n

設置的 G1 區域的大小。值是 2 的冪,範圍是 1 MB 到 32 MB 之間。目標是根據最小的 Java 堆大小劃分出約 2048 個區域。

-XX:ParallelGCThreads=n(調整G1垃圾收集的後臺線程數)

設置 STW 工作線程數的值。將 n 的值設置爲邏輯處理器的數量。n 的值與邏輯處理器的數量相同,最多爲 8。

如果邏輯處理器不止八個,則將 n 的值設置爲邏輯處理器數的 5/8 左右。這適用於大多數情況,除非是較大的 SPARC 系統,其中 n 的值可以是邏輯處理器數的 5/16 左右。

-XX:ConcGCThreads=n(調整G1垃圾收集的後臺線程數)

設置並行標記的線程數。將 n 設置爲並行垃圾回收線程數 (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45(調整G1垃圾收集運行頻率)

設置觸發標記週期的 Java 堆佔用率閾值。默認佔用率是整個 Java 堆的 45%。

該值設置太高:會陷入Full GC泥潭之中,因爲併發階段沒有足夠的時間在剩下的堆空間被填滿之前完成垃圾收集。

如果該值設置太小:應用程序又會以超過實際需要的節奏進行大量的後臺處理。

避免使用以下參數:

避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小。固定年輕代的大小會覆蓋暫停時間目標。

-XX:G1MixedGCLiveThresholdPercent=65

 

爲混合垃圾回收週期中要包括的舊區域設置佔用率閾值。默認佔用率爲 65%。這是一個實驗性的標誌。有關示例,請參見“如何解鎖實驗性虛擬機標誌”。此設置取代了 -XX:G1OldCSetRegionLiveThresholdPercent 設置。Java HotSpot VM build 23 中沒有此設置。

-XX:G1MixedGCCountTarget=8

設置標記週期完成後,對存活數據上限爲 G1MixedGCLIveThresholdPercent 的舊區域執行混合垃圾回收的目標次數。默認值是 8 次混合垃圾回收。混合回收的目標是要控制在此目標次數以內。Java HotSpot VM build 23 中沒有此設置。

-XX:G1OldCSetRegionThresholdPercent=10

設置混合垃圾回收期間要回收的最大舊區域數。默認值是 Java 堆的 10%。Java HotSpot VM build 23 中沒有此設置。

-XX:G1ReservePercent=10

設置作爲空閒空間的預留內存百分比,以降低目標空間溢出的風險。默認值是 10%。增加或減少百分比時,請確保對總的 Java 堆調整相同的量。Java HotSpot VM build 23 中沒有此設置。

 

 

 

-XX:G1HeapWastePercent=10

設置您願意浪費的堆百分比。如果可回收百分比小於堆廢物百分比,Java HotSpot VM 不會啓動混合垃圾回收週期。默認值是 10%。Java HotSpot VM build 23 中沒有此設置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章