天天看點

垃圾收集器之:G1收集器

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

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

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

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

新生代垃圾收集;

背景收集,并發周期;

混合式垃圾收集;

以及必要時的Full GC。

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

垃圾收集器之:G1收集器
垃圾收集器之:G1收集器

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

PS:在java 8中,持久代也移動到了普通的堆記憶體空間中,改為元空間。

說起大對象的配置設定,我們不得不談談對象的配置設定政策。它分為3個階段:

TLAB(Thread Local Allocation Buffer)線程本地配置設定緩沖區

Eden區中配置設定

Humongous區配置設定

TLAB為線程本地配置設定緩沖區,它的目的為了使對象盡可能快的配置設定出來。如果對象在一個共享的空間中配置設定,我們需要采用一些同步機制來管理這些空間内的空閑空間指針。在Eden空間中,每一個線程都有一個固定的分區用于配置設定對象,即一個TLAB。配置設定對象時,線程之間不再需要進行任何的同步。

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

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

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

垃圾收集器之:G1收集器
垃圾收集器之:G1收集器

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

垃圾收集器之:G1收集器

在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:處理引用隊列

軟引用,弱引用,虛引用處理

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

它的GC步驟分2步:

全局并發标記(global concurrent marking)

拷貝存活對象(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開始掃描對象時,按照如下圖步驟進行對象的掃描:

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

繼續由灰色周遊,将已掃描了子對象的對象置為黑色。

垃圾收集器之:G1收集器

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

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

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

垃圾收集器之:G1收集器

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

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

垃圾收集器之:G1收集器

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

垃圾收集器之:G1收集器

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

在插入的時候記錄對象

在删除的時候記錄對象

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

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

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

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

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

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

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

垃圾收集器之:G1收集器

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

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

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

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

GC日志如下的示例:

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

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

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

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

垃圾收集器之:G1收集器

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

解決這種問題的方式是:

增加 -XX:G1ReservePercent 選項的值(并相應增加總的堆大小),為“目标空間”增加預留記憶體量。

通過減少 -XX:InitiatingHeapOccupancyPercent 提前啟動标記周期。

也可以通過增加 -XX:ConcGCThreads 選項的值來增加并行标記線程的數目。

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

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

當巨型對象找不到合适的空間進行配置設定時,就會啟動Full GC,來釋放空間。這種情況下,應該避免配置設定大量的巨型對象,增加記憶體或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。

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

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

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

通過增加總的堆空間大小夥子調整老年代、新生代之間的比例來增加老年代空間的大小。

增加背景線程的數位(假設我們有足夠的CPU資源運作這些線程)。

以更高的頻率進行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

-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毫秒,我們可以從這裡入手,調整合适的時間。

-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 左右。

垃圾收集器之:G1收集器

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

設定觸發标記周期的 Java 堆占用率門檻值。預設占用率是整個 Java 堆的 45%。

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

如果該值設定太小:應用程式又會以超過實際需要的節奏進行大量的背景處理。

避免使用以下參數:

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

-XX:G1MixedGCLiveThresholdPercent=65

為混合垃圾回收周期中要包括的舊區域設定占用率門檻值。預設占用率為 65%。這是一個實驗性的标志。有關示例,請參見“如何解鎖實驗性虛拟機标志”。此設定取代了 <code>-XX:G1OldCSetRegionLiveThresholdPercent</code> 設定。Java HotSpot VM build 23 中沒有此設定。

-XX:G1MixedGCCountTarget=8

設定标記周期完成後,對存活資料上限為 <code>G1MixedGCLIveThresholdPercent</code> 的舊區域執行混合垃圾回收的目标次數。預設值是 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 中沒有此設定。

下一篇: js小知識

繼續閱讀