沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

今日分享開始啦,請大家多多指教~

垃圾收集器沒有在規範中進行過多的規定,可以由不同的廠商、不同版本的JVM來實現。由於JDK的版本處於高速迭代過程中,因此Java發展至今已經衍生了眾多的GC版本。從不同角度分析垃圾收集器,可以將GC分為不同的型別。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

垃圾收集器分類

按執行緒數分: 序列垃圾回收器和並行垃圾回收器。

序列回收: 在同一時間段內只允許有一個CPU用於執行垃圾回收操作,此時工作執行緒被暫停,直至垃圾收集工作結束。在諸如單CPU處理器或者較小的應用記憶體等硬體平臺不是特別優越的場合,序列回收器的效能表現可以超過並行回收器和併發回收器。所以,序列回收預設被應用在客戶端的Client模式下的JVM中。

並行回收: 可以運用多個CPU同時執行垃圾回收,因此提升了應用的吞吐量,不過並行回收仍然與序列回收一樣,採用獨佔式,使用了“stop-the-world”機制。在併發能力比較強的CPU上,並行回收器產生的停頓時間要短於序列回收器。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

按工作模式分: 併發式垃圾回收器和獨佔式垃圾回收器。

併發式垃圾回收器與應用程式執行緒交替工作,以儘可能減少應用程式的停頓時間。

獨佔式垃圾回收器(Stop the world) 一旦執行,就停止應用程式中的所有使用者執行緒,直到垃圾回收過程完全結束。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

按碎片處理方式分: 壓縮式垃圾回收器和非壓縮式垃圾回收器。

壓縮式垃圾回收器會在回收完成後,對存活物件進行壓縮整理,消除回收後的碎片。再分配物件空間採用指標碰撞。

非壓縮式的垃圾回收器不進行這步操作。再分配物件空間採用空閒列表。

按工作的記憶體區間分:年輕代垃圾回收器和老年代垃圾回收器。

評估GC的效能指標

吞吐量:執行使用者程式碼的時間佔總執行時間的比例(總執行時間 = 程式的執行時間 + 記憶體回收的時間)

暫停時間:執行垃圾收集時,程式的工作執行緒被暫停的時間。

記憶體佔用:Java堆區所佔的記憶體大小。

垃圾收集開銷:吞吐量的步數,垃圾收集所用時間與總執行時間的比例。

收集頻率:相對於應用程式的執行,收集操作發生的頻率。

快速:一個物件從誕生到被回收所經歷的時間。

吞吐量、暫停時間、記憶體佔用這三者共同構成一個“不可能三角”。一款優秀的收集器通常最多同時滿足其中的兩項。這三項裡,暫停時間的重要性日益凸顯。因為隨著硬體發展,記憶體佔用多些越來越能容忍,硬體效能的提升也有助於降低收集器執行時對應用程式的影響,即提高了吞吐量。而記憶體的擴大,對延遲反而帶來負面效果。

簡單來說,主要抓住兩點:吞吐量、暫停時間。

吞吐量:

吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間 /(執行使用者程式碼時間+垃圾收集時間)

比如:虛擬機器總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

這種情況下,應用程式能容忍較高的暫停時間,因此,高吞吐量的應用程式有更長的時間基準,快速響應是不必考慮的。

吞吐量優先,意味著在單位時間內,STW的時間最短:0。2+0。2=0。4

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

暫停時間:

“暫停時間”是指一個時間段內應用程式執行緒暫停,讓GC執行緒執行的狀態。

例如,GC期間1ee毫秒的暫停時間意味著在這1e0毫秒期間內沒有應用程式執行緒是活動的。暫停時間優先,意味著儘可能讓單次STW的時間最短:0。1+0。1 + 0。1+ 0。1+ 0。1=0。5

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

吞吐量vs暫停時間

高吞吐量較好因為這會讓應用程式的終端使用者感覺只有應用程式執行緒在做“生產性”工作。直覺上,吞吐量越高程式執行越快。

低暫停時間(低延遲)較好因為從終端使用者的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。這取決於應用程式的型別,有時候甚至短暫的200毫秒暫停都可能打斷終端使用者體驗。因此,具有低的較大暫停時間是非常重要的,特別是對於一個互動式應用程式。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

不幸的是”高吞吐量”和”低暫停時間”是一對相互競爭的目標(矛盾)。

因為如果選擇以吞吐量優先,那麼必然需要降低記憶體回收的執行頻率,但是這樣會導致GC需要更長的暫停時間來執行記憶體回收。

相反的,如果選擇以低延遲優先為原則,那麼為了降低每次執行記憶體回收時的暫停時間,也只能頻繁地執行記憶體回收,但這又引起了年輕代記憶體的縮減和導致程式吞吐量的下降。

在設計(或使用)GC演算法時,我們必須確定我們的目標:一個GC演算法只可能專注於兩個目標之一,或嘗試找到一個二者的折衷。

現在標準:在最大吞吐量優先的情況下,降低停頓時間。

常見的垃圾回收器

GC垃圾收集器是和JVM一脈相承的,它是和JVM進行搭配使用,在不同的使用場景對應的收集器也是有區別。

7種經典的垃圾收集器:

序列回收器:Serial、Serial old

並行回收器:ParNew、Parallel Scavenge、Parallel old

併發回收器:CMS、G1

雖然我們會對各個收集器進行比較,但並非為了挑選一個最好的收集器出來。沒有一種任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。所以我們選擇的只是對具體應用最合適的收集器。

如何檢視預設垃圾收集器:

-XX:+PrintcommandLineFlags檢視命令列相關引數(包含使用的垃圾收集器)

使用命令列指令:jinfo -flag 相關垃圾回收器引數程序ID

Serial回收器:序列回收

Serial收集器是最基本、歷史最悠久的垃圾收集器了,JDK1。3之前回收新生代唯一的選擇。它是HotSpot中Client模式下的預設新生代垃圾收集器,採用複製演算法、序列回收和“stop-the-World”機制的方式執行記憶體回收。

除了年輕代之外,Serial收集器還提供用於執行老年代垃圾收集的Serial old收集器。Serial old收集器同樣也採用了序列回收和“Stop the World”機制,只不過記憶體回收演算法使用的是標記-壓縮演算法。

Serial Old是執行在Client模式下預設的老年代的垃圾回收器。

Serial Old在Server模式下主要有兩個用途:與新生代的Parallel scavenge配合使用,作為老年代CMS收集器的後備垃圾收集方案。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

Serial收集器是一個單執行緒的收集器,但它的“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束(Stop The World)。

優勢: 簡單而高效(與其他收集器的單執行緒比),對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。

限制條件:序列,單核CPU,不支援互動較強的應用。

引數設定:

-XX:+UseSerialGC :指定年輕代和老年代都使用序列收集器(等價於新生代用Serial GC,且老年代用Serial old GC)

ParNew回收器:並行回收

ParNew收集器可以看作是Serial收集器的多執行緒版本。Par是Parallel的縮寫,New:只能處理的是新生代。ParNew 收集器除了採用並行回收的方式執行記憶體回收外,兩款垃圾收集器之間幾乎沒有任何區別。ParNew收集器在年輕代中同樣也是採用複製演算法、“Stop-the-World”機制。

ParNew 是很多JVM執行在Server模式下新生代的預設垃圾收集器。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

對於新生代,回收次數頻繁,使用並行方式高效。

對於老年代,回收次數少,使用序列方式節省資源。(CPU並行需要切換執行緒,序列可以省去切換執行緒的資源)

由於ParNew收集器是基於並行回收,那麼是否可以斷定ParNew收集器的回收效率在任何場景下都會比serial收集器更高效?

ParNew收集器執行在多CPU的環境下,由於可以充分利用多CPU、多核心等物理硬體資源優勢,可以更快地完成垃圾收集,提升程式的吞吐量。但是在單個CPU環境下,PerNew收集器不比Serial收集器更高效。 雖然Serial收集器時基於序列回收,但是由於CPU不需要頻繁地做任務切換,因此可以有效避免多執行緒中產生的一些額外開銷。

引數設定:

-XX:+UseParNewGC:指定使用ParNew收集器執行記憶體回收任務。它表示年輕代使用並行收集器,不影響老年代。

-XX:ParallelGCThreads:限制執行緒數量,預設開啟和CPU資料相同的執行緒數。

Parallel回收器:吞吐量優先

Parallel Scavenge收集器中採用了複製演算法、並行回收和“Stop the World”機制。 和ParNew收集器不同,ParallelScavenge收集器的目標是達到一個可控制的吞吐量(Throughput),它也被稱為吞吐量優先的垃圾收集器。

自適應調節策略也是Parallel Scavenge與ParNew一個重要區別。

高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。因此,常見在伺服器環境中使用。例如,那些執行批次處理、訂單處理、工資支付、科學計算的應用程式。

Parallel收集器在JDK1。6時提供了用於執行老年代垃圾收集的Parallel old收集器,用來代替老年代的Serialold收集器。

Parallel old收集器採用了標記-壓縮演算法,但同樣也是基於並行回收和“stop-the-World”機制。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

在程式吞吐量優先的應用場景中,IParallel收集器和Parallel old收集器的組合,在server模式下的記憶體回收效能很不錯。在Java8中,預設是此垃圾收集器。

引數配置:

-XX:+UseParallelGC: 手動指定年輕代使用Parallel並行收集器執行記憶體回收任務,適用於新生代。

-XX:+UseParalleloldcc :手動指定老年代都是使用並行回收收集器,適用於老年代。兩個引數,預設jdk8是開啟的,開啟一個,另一個也會被開啟。(互相啟用)

-XX:ParallelGcrhreads:設定年輕代並行收集器的執行緒數。一般地,最好與CPU數量相等,以避免過多的執行緒數影響垃圾收集效能。在預設情況下,當CPU數量小於8個,ParallelGcThreads的值等於CPU數量。當CPU數量大於8個,ParallelGCThreads的值等於3+[5*CPU Count]/8]

-XX:MaxGCPauseMillis 設定垃圾收集器最大停頓時間(即STW的時間),單位是毫秒。為了儘可能地把停頓時間控制在MaxGCPauseMills以內,收集器在工作時會調整Java堆大小或者其他一些引數。對於使用者來講,停頓時間越短體驗越好。但是在伺服器端,我們注重高併發,整體的吞吐量。所以伺服器端適合Parallel,進行控制。該引數使用需謹慎。

-XX:GCTimeRatio:垃圾收集時間佔總時間的比例(=1/(N+1))。用於衡量吞吐量的大小。取值範圍(0,100)。預設值99,也就是垃圾回收時間不超過1。與前一個-xx:MaxGCPauseMillis引數有一定矛盾性。暫停時間越長,Radio引數就容易超過設定的比例。

-XX:+UseAdaptivesizepplicy :設定Parallel scavenge收集器具有自適應調節策略,在這種模式下,年輕代的大小、Eden和Survivor的比例、晉升老年代的物件年齡等引數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機器的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMil1s),讓虛擬機器自己完成調優工作。

CMS回收器:低延遲

在JDK1。5時期,Hotspot推出了一款在強互動應用中幾乎可認為有劃時代意義的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,這款收集器是HotSpot虛擬機器中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒同時工作。

CMS收集器的關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間。停頓時間越短(低延遲)就越適合與使用者互動的程式,良好的響應速度能提升使用者體驗。

目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。 CMS收集器就非常符合這類應用的需求。

CMS的垃圾收集演算法採用標記-清除演算法,並且也會“stop-the-world”

CMS整個過程比之前的收集器要複雜,整個過程分為4個主要階段,即初始標記階段、併發標記階段、重新標記階段和併發清除階段。(涉及STW的階段主要是:初始標記和重新標記)

初始標記(Initial-Mark)階段: 在這個階段中,程式中所有的工作執行緒都將會因為“stop-the-world”機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記出GCRoots能直接關聯到的物件。一旦標記完成之後就會恢復之前被暫停的所有應用執行緒。由於直接關聯物件比較小,所以這裡的速度非常快。

併發標記(Concurrent-Mark)階段: 從Gc Roots的直接關聯物件開始遍歷整個物件圖的過程,這個過程耗時較長但是不需要停頓使用者執行緒,可以與垃圾收集執行緒一起併發執行。

重新標記(Remark)階段: 由於在併發標記階段中,程式的工作執行緒會和垃圾收集執行緒同時執行或者交叉執行,因此為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間(STW)通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。

併發清除(Concurrent-Sweep)階段: 此階段清理刪除掉標記階段判斷的已經死亡的物件,釋放記憶體空間。由於不需要移動存活物件,所以這個階段也是可以與使用者執行緒同時併發的。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

儘管CMS收集器採用的是併發回收(非獨佔式),但是在其初始化標記和再次標記這兩個階段中仍然需要執行“Stop-the-World”機制暫停程式中的工作執行緒,不過暫停時間並不會太長,因此可以說明目前所有的垃圾收集器都做不到完全不需要“stop-the-World”,只是儘可能地縮短暫停時間。由於最耗費時間的併發標記與併發清除階段都不需要暫停工作,所以整體的回收是低停頓的。

另外,由於在垃圾收集階段使用者執行緒沒有中斷,所以在CMS回收過程中,還應該確保應用程式使用者執行緒有足夠的記憶體可用。 因此,CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當堆記憶體使用率達到某一閾值時,便開始進行回收, 以確保應用程式在CMS工作過程中依然有足夠的空間支援應用程式執行。

要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次 “Concurrent Mode Failure” 失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

CMS收集器的垃圾收集演算法採用的是標記清除演算法,這意味著每次執行完記憶體回收後,由於被執行記憶體回收的無用物件所佔用的記憶體空間極有可能是不連續的一些記憶體塊,不可避免地將會產生一些記憶體碎片。那麼CMS在為新物件分配記憶體空間時,將無法使用指標碰撞(Bump the Pointer)技術,而只能夠選擇空閒列表(Free List)執行記憶體分配。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

CMS為什麼不使用標記整理演算法?

當併發清除的時候,用Compact整理記憶體的話,會佔用原來的使用者執行緒使用的記憶體(要保證使用者執行緒能繼續執行,前提的它執行的資源不受影響)。Mark Compact更適合“stop the world”這種場景下使用。

優點:併發收集、低延遲

缺點:

會產生記憶體碎片,導致併發清除後,使用者執行緒可用的空間不足。在無法分配大物件的情況下,不得不提前觸發FullGC。

CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致使用者停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低。

CMS收集器無法處理浮動垃圾。可能出現“Concurrent Mode Failure“失敗而導致另一次Full GC的產生。在併發標記階段由於程式的工作執行緒和垃圾收集執行緒是同時執行或者交叉執行的,那麼在併發標記階段如果產生新的垃圾物件,CMS將無法對這些垃圾物件進行標記,最終會導致這些新產生的垃圾物件沒有被及時回收,從而只能在下一次執行GC時釋放這些之前未被回收的記憶體空間。

引數設定:

-XX:+UseConcMarkSweepGC:手動指定使用CMS收集器執行記憶體回收任務。開啟該引數後會自動將-xx:+UseParNewGC開啟。即:ParNew(Young區用)+CMS(01d區用)+Serial old的組合。

-XX:CMSInitiatingoccupanyFraction: 設定堆記憶體使用率的閾值,一旦達到該閾值,便開始進行回收。

JDK5及以前版本的預設值為68,即當老年代的空間使用率達到68%時,會執行一次cMs回收。JDK6及以上版本預設值為92%。如果記憶體增長緩慢,則可以設定一個稍大的值,大的閥值可以有效降低CMS的觸發頻率,減少老年代回收的次數可以較為明顯地改善應用程式效能。反之,如果應用程式記憶體使用率增長很快,則應該降低這個閾值,以避免頻繁觸發老年代序列收集器。因此透過該選項便可以有效降低FullGC的執行次數。

-XX:+UseCMSCompactAtFullCollection:用於指定在執行完FullGC後對記憶體空間進行壓縮整理,以此避免記憶體碎片的產生。不過由於記憶體壓縮整理過程無法併發執行,所帶來的問題就是停頓時間變得更長了。

-XX:CMSFullGCsBeforecompaction: 設定在執行多少次FullGC後對記憶體空間進行壓縮整理。

-XX:ParallelcMSThreads: 設定CMS的執行緒數量。CMS預設啟動的執行緒數是(ParallelGCThreads+3)/4,ParallelGCThreads是年輕代並行收集器的執行緒數。當CPU資源比較緊張時,受到CMS收集器執行緒的影響,應用程式的效能在垃圾回收階段可能會非常糟糕。

如果你想要最小化地使用記憶體和並行開銷,請選Serial GC;

如果你想要最大化應用程式的吞吐量,請選Parallel GC;

如果你想要最小化GC的中斷或停頓時間,請選CMS GC。

G1回收器:區域化分代式

隨著應用程式所應對的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式正常進行,而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行最佳化。G1(Garbage-First)垃圾回收器是在Java7 update4之後引入的一個新的垃圾回收器,是當今收集器技術發展的最前沿成果之一。

與此同時,為了適應現在不斷擴大的記憶體和不斷增加的處理器數量,進一步降低暫停時間(pause time),同時兼顧良好的吞吐量。

官方給G1設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才擔當起“全功能收集器”的重任與期望。

為什麼名字叫 Garbage First(G1)呢?

因為G1是一個並行回收器,它把堆記憶體分割為很多不相關的區域(Region)(物理上不連續的)。使用不同的Region來表示Eden、倖存者0區,倖存者1區,老年代等。

G1 GC有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。

由於這種方式的側重點在於回收垃圾最大量的區間(Region),所以我們給G1一個名字:垃圾優先(Garbage First)。

G1(Garbage-First)是一款面向服務端應用的垃圾收集器,主要針對配備多核CPU及大容量記憶體的機器,以極高機率滿足GC停頓時間的同時,還兼具高吞吐量的效能特徵。

在JDK1。7版本正式啟用,移除了Experimental的標識,是JDK9以後的預設垃圾回收器,取代了CMS回收器以及Parallel+Parallel old組合。被oracle官方稱為 “全功能的垃圾收集器”。

G1垃圾收集器的優點:

並行與併發:

並行性: G1在回收期間,可以有多個GC執行緒同時工作,有效利用多核計算能力。此時使用者執行緒STW。

併發性: G1擁有與應用程式交替執行的能力,部分工作可以和應用程式同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程式的情況。

分代收集:

從分代上看,G1依然屬於分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和Survivor區。但從堆的結構上看,它不要求整個Eden區、年輕代或者老年代都是連續的,也不再堅持固定大小和固定數量。

將堆空間分為若干個區域(Region),這些區域中包含了邏輯上的年輕代和老年代。

和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

空間整合:

CMS: “標記-清除”演算法、記憶體碎片、若干次GC後進行一次碎片整理。

G1將記憶體劃分為一個個的region。記憶體的回收是以region作為基本單位的。Region之間是複製演算法,但整體上實際可看作是標記-壓縮(Mark-Compact)演算法,兩種演算法都可以避免記憶體碎片。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。尤其是當Java堆非常大的時候,G1的優勢更加明顯。

可預測的停頓時間模型(即:軟實時soft real-time):

這是G1相對於CMS的另一大優勢,G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

由於分割槽的原因,G1可以只選取部分割槽域進行記憶體回收,這樣縮小了回收的範圍,因此對於全域性停頓情況的發生也能得到較好的控制。

G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

相比於CMSGC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。

G1垃圾收集器的缺點:

相較於CMS,G1還不具備全方位、壓倒性優勢。比如在使用者程式執行過程中,G1無論是為了垃圾收集產生的記憶體佔用(Footprint)還是程式執行時的額外執行負載(overload)都要比CMS要高。

從經驗上來說,在小記憶體應用上CMS的表現大機率會優於G1,而G1在大記憶體應用上則發揮其優勢。平衡點在6-8GB之間。

G1引數設定:

-XX:+UseG1GC:手動指定使用G1垃圾收集器執行記憶體回收任務

-XX:G1HeapRegionSize:設定每個Region的大小。值是2的冪,範圍是1MB到32MB之間,目標是根據最小的Java堆大小劃分出約2048個區域。預設是堆記憶體的1/2000。

-XX:MaxGCPauseMillis: 設定期望達到的最大Gc停頓時間指標(JVM會盡力實現,但不保證達到)。預設值是200ms

-XX:+ParallelGcThread: 設定STW工作執行緒數的值。最多設定為8

-XX:ConcGCThreads: 設定併發標記的執行緒數。將n設定為並行垃圾回收執行緒數(ParallelGcThreads)的1/4左右。

-XX:InitiatingHeapoccupancyPercent:設定觸發併發Gc週期的Java堆佔用率閾值。超過此值,就觸發GC。預設值是45。

G1收集器的常見操作步驟:

第一步:開啟G1垃圾收集器;

第二步:設定堆的最大記憶體;

第三步:設定最大的停頓時間。

G1中提供了三種垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同的條件下被觸發。

G1收集器的適用場景:

面向服務端應用,針對具有大記憶體、多處理器的機器。(在普通大小的堆裡表現並不驚喜)

最主要的應用是需要低GC延遲,並具有大堆的應用程式提供解決方案;

如:在堆大小約6GB或更大時,可預測的暫停時間可以低於e。5秒;(G1透過每次只清理一部分而不是全部的Region的增量式清理來保證每次Gc停頓時間不會過長)。

用來替換掉JDK1。5中的CMS收集器;在下面的情況時,使用61可能比CMS好:

①超過5e%的Java堆被活動資料佔用;

②物件分配頻率或年代提升頻率變化很大;

③GC停頓時間過長(長於e。5至1秒)

HotSpot垃圾收集器裡,除了61以外,其他的垃圾收集器使用內建的JVM執行緒執行Gc的多執行緒操作,而G1GC可以採用應用執行緒承擔後臺執行的GC工作,即當JVM的GC執行緒處理速度慢時,系統會呼叫應用程式執行緒幫助加速垃圾回收過程。

分割槽Region:化整為零

使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據堆空間的實際大小而定,整體被控制在1MB到32MB之間,且為2的N次冪,即1MB,2MB,4MB,8MB,16MB,32MB。可以透過XX:G1HeapRegionsize設定。所有的Region大小相同,且在JVM生命週期內不會被改變。

雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。透過Region的動態分配方式實現邏輯上的連續。

一個region有可能屬於Eden,Survivor或者old/Tenured記憶體區域。但是一個region只可能屬於一個角色。圖中的E表示該region屬於Eden記憶體區域,s表示屬於survivor記憶體區域,o表示屬於0ld記憶體區域。圖中空白地表示未使用的記憶體空間。

G1垃圾收集器還增加了一種新的記憶體區域,叫做Humongous記憶體區域,如圖中的H塊。主要用於儲存大物件,如果超過1。5個region,就放到H。

設定H的原因:

對於堆中的物件,預設直接會被分配到老年代,但是如果它是一個短期存在的大物件就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放大物件。**如果一個H區裝不下一個大物件,那麼G1會尋找連續的H區來儲存。**為了能找到連續的H區,有時候不得不啟動Fu11Gc。G1的大多數行為都把H區作為老年代的一部分來看待。

每個Region都是透過指標碰撞來分配空間。

G1垃圾回收器的回收過程

年輕代GC(Young GC)

老年代併發標記過程(Concurrent Marking)

混合回收(Mixed GC)

(如果需要,單執行緒、獨佔式、高強度的FullGC還是繼續存在的。它針對GC的評估失敗提供了一種失敗保護機制,即強力回收。)

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

應用程式分配記憶體,當年輕代的Eden區用盡時開始年輕代回收過程;G1的年輕代收集階段是一個並行的獨佔式收集器。在年輕代回收期,G1GC暫停所有應用程式執行緒,啟動多執行緒執行年輕代回收。然後從年輕代區間移動存活物件到Survivor區間或者老年區間,也有可能是兩個區間都會涉及。

當堆記憶體使用達到一定值(預設45%)時,開始老年代併發標記過程。

標記完成馬上開始混合回收過程。對於一個混合回收期,G1GC從老年區間移動存活物件到空閒區間,這些空閒區間也就成為了老年代的一部分。和年輕代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收一小部分老年代的Region就可以了。同時,這個老年代Region是和年輕代一起被回收的。

舉個例子: 一個Web伺服器,Java程序最大堆記憶體為4G,每分鐘響應1500個請求,每45秒鐘會新分配大約2G的記憶體。G1會每45秒鐘進行一次年輕代回收,每31個小時整個堆的使用率會達到45%,會開始老年代併發標記過程,標記完成後開始四到五次的混合回收。

Remembered Set(記憶集):

一個物件被不同區域引用的問題;

一個Region不可能是孤立的,一個Region中的物件可能被其他任意Region中物件引用,判斷物件存活時,是否需要掃描整個Java堆才能保證準確?

在其他的分代收集器,也存在這樣的問題(而G1更突出)回收新生代也不得不同時掃描老年代?這樣的話會降低MinorGC的效率;

解決方法:

無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全域性掃描:

每個Region都有一個對應的Remembered Set;

每次Reference型別資料寫操作時,都會產生一個Write Barrier暫時中斷操作;

然後檢查將要寫入的引用指向的物件是否和該Reference型別資料在不同的Region(其他收集器:檢查老年代物件是否引用了新生代物件);

如果不同,透過cardTable把相關引用資訊記錄到引用指向物件的所在Region對應的Remembered Set中;

當進行垃圾收集時,在GC根節點的列舉範圍加入Remembered Set;

就可以保證不進行全域性掃描,也不會有遺漏。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

G1回收過程1 - 年輕代GC

JVM啟動時,G1先準備好Eden區,程式在執行過程中不斷建立物件到Eden區,當Eden空間耗盡時,G1會啟動一次年輕代垃圾回收過程。年輕代回收只會回收Eden區和Survivor區。

YGC時,首先G1停止應用程式的執行(stop-The-Wor1d),G1建立回收集(Collection Set),回收集是指需要被回收的記憶體分段的集合,年輕代回收過程的回收集包含年輕代Eden區和Survivor區所有的記憶體分段。

然後開始如下回收過程:

第一階段,掃描根

根是指static變數指向的物件,正在執行的方法呼叫鏈條上的區域性變數等。根引用連同RSet記錄的外部引用作為掃描存活物件的入口。

第二階段,更新RSet

處理dirty card queue(見備註)中的card,更新RSet。此階段完成後,RSet可以準確地反映老年代對所在的記憶體分段中物件的引用。

第三階段,處理RSet

識別被老年代物件指向的Eden中的物件,這些被指向的Eden中的物件被認為是存活的物件。

第四階段,複製物件

此階段,物件樹被遍歷,Eden區記憶體段中存活的物件會被複制到Survivor區中空的記憶體分段,Survivor區記憶體段中存活的物件如果年齡未達閾值,年齡會加1,達到閥值會被會被複制到o1d區中空的記憶體分段。如果Survivor空間不夠,Eden空間的部分資料會直接晉升到老年代空間。

第五階段,處理引用

處理Soft,Weak,Phantom,Final,JNI Weak 等引用。最終Eden空間的資料為空,GC停止工作,而目標記憶體中的物件都是連續儲存的,沒有碎片,所以複製過程可以達到記憶體整理的效果,減少碎片。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

G1回收過程2 - 併發標記過程:

初始標記階段: 標記從根節點直接可達的物件。這個階段是sTw的,並且會觸發一次年輕代GC。

根區域掃描(Root Region Scanning): G1 Gc掃描survivor區直接可達的老年代區域物件,並標記被引用的物件。這一過程必須在youngGC之前完成。

併發標記(Concurrent Marking): 在整個堆中進行併發標記(和應用程式併發執行),此過程可能被youngGC中斷。在併發標記階段,若發現區域物件中的所有物件都是垃圾,那這個區域會被立即回收。 同時,併發標記過程中,會計算每個區域的物件活性(區域中存活物件的比例)。

再次標記(Remark): 由於應用程式持續進行,需要修正上一次的標記結果。是STW的。G1中採用了比CMS更快的初始快照演算法:snapshot-at-the-beginning(SATB)。

獨佔清理(cleanup,STW): 計算各個區域的存活物件和GC回收比例,並進行排序,識別可以混合回收的區域。為下階段做鋪墊。是sTw的。這個階段並不會實際上去做垃圾的收集

併發清理階段: 識別並清理完全空閒的區域。

G1回收過程3 - 混合回收:

當越來越多的物件晉升到老年代o1d region時,為了避免堆記憶體被耗盡,虛擬機器會觸發一個混合的垃圾收集器,即Mixed GC,該演算法並不是一個old GC,除了回收整個Young Region,還會回收一部分的old Region。這裡需要注意:是一部分老年代,而不是全部老年代。可以選擇哪些o1d Region進行收集,從而可以對垃圾回收的耗時時間進行控制。也要注意的是Mixed GC並不是Full GC。

併發標記結束以後,老年代中百分百為垃圾的記憶體分段被回收了,部分為垃圾的記憶體分段被計算了出來。預設情況下,這些老年代的記憶體分段會分8次(可以透過-XX:G1MixedGCCountTarget設定)被回收

混合回收的回收集(Collection Set)包括八分之一的老年代記憶體分段,Eden區記憶體分段,Survivor區記憶體分段。混合回收的演算法和年輕代回收的演算法完全一樣,只是回收集多了老年代的記憶體分段。

由於老年代中的記憶體分段預設分8次回收,G1會優先回收垃圾多的記憶體分段。垃圾佔記憶體分段比例越高的,越會被先回收。並且有一個閾值會決定記憶體分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,預設為65%,意思是垃圾佔記憶體分段比例要達到65%才會被回收。如果垃圾佔比太低,意味著存活的物件佔比高,在複製的時候會花費更多的時間。

混合回收並不一定要進行8次。有一個閾值-XX:G1HeapWastePercent,預設值為1e%,意思是允許整個堆記憶體中有10%的空間被浪費,意味著如果發現可以回收的垃圾佔堆記憶體的比例低於1e%,則不再進行混合回收。因為GC會花費很多的時間但是回收到的記憶體卻很少。

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

G1回收可選的過程4 - Full GC:

G1的初衷就是要避免FullGC的出現。但是如果上述方式不能正常工作,G1會停止應用程式的執行(stop-The-world),使用單執行緒的記憶體回收演算法進行垃圾回收,效能會非常差,應用程式停頓時間會很長。

要避免FullGC的發生,一旦發生需要進行調整。什麼時候會發生Ful1GC呢?比如堆記憶體太小,當G1在複製存活物件的時候沒有空的記憶體分段可用,則會回退到ful1gc,這種情況可以透過增大記憶體解決。

導致G1FullGC的原因可能有兩個:

①EVacuation的時候沒有足夠的to-space來存放晉升的物件;

②併發處理過程完成之前空間耗盡。

G1回收的最佳化建議:

從oracle官方透露出來的資訊可獲知,回收階段(Evacuation)其實本也有想過設計成與使用者程式一起併發執行,但這件事情做起來比較複雜,考慮到G1只是回一部分Region,停頓時間是使用者可控制的,所以並不迫切去實現,而選擇把這個特性放到了G1之後出現的低延遲垃圾收集器(即ZGC)中。另外,還考慮到G1不是僅僅面向低延遲,停頓使用者執行緒能夠最大幅度提高垃圾收集效率,為了保證吞吐量所以才選擇了完全暫停使用者執行緒的實現方案。

年輕代大小:

避免使用-Xmn或-XX:NewRatio等相關選項顯式設定年輕代大小;

固定年輕代的大小會覆蓋。

暫停時間目標暫停時間目標不要太過嚴苛:

G1 GC的吞吐量目標是90%的應用程式時間和10%的垃圾回收時間;

評估G1GC的吞吐量時,暫停時間目標不要太嚴苛。目標太過嚴苛表示你願意承受更多的垃圾回收開銷,而這些會直接影響到吞吐量。

垃圾回收器總結

截止JDK1。8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特點,在具體使用的時候,需要根據具體的情況選用不同的垃圾收集器。

怎麼選擇垃圾回收器?

Java垃圾收集器的配置對於JVM最佳化來說是一個很重要的選擇,選擇合適的垃圾收集器可以讓JVM的效能有一個很大的提升。怎麼選擇垃圾收集器?

優先調整堆的大小讓JVM自適應完成。

如果記憶體小於100M,使用序列收集器

如果是單核、單機程式,並且沒有停頓時間的要求,序列收集器

如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇並行或者JVM自己選擇

如果是多CPU、追求低停頓時間,需快速響應(比如延遲不能超過1秒,如網際網路應用),使用併發收集器

官方推薦G1,效能高。現在網際網路的專案,基本都是使用G1。

GC日誌

透過閱讀GC日誌,我們可以瞭解Java虛擬機器記憶體分配與回收策略。

記憶體分配與垃圾回收的引數列表:

-XX:+PrintGC:輸出GC日誌。類似:-verbose:gc

-XX:+PrintGcDetails:輸出Gc的詳細日誌

-XX:+PrintGCTimestamps: 輸出GC的時間戳(以基準時間的形式)

-XX:+PrintGCDatestamps :輸出GC的時間戳(以日期的形式,如2013-05-04T21:53:59。234+0800)

-XX:+PrintHeapAtGC:在進行GC的前後打印出堆的資訊

-Xloggc:…/logs/gc。1og:日誌檔案的輸出路徑

YoungGC:

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

FullGC:

沒學會?華為大佬梳理的這份萬字JVM筆記,帶你掌握7種垃圾回收器

垃圾回收器的新發展

GC仍然處於飛速發展之中,目前的預設選項G1GC在不斷地進行改進,很多我們原來認為的缺點,例如序列的FullGC、Card Table掃描的低效等,都已經被大幅改進,例如,JDK10以後,FullGC已經是並行執行,在很多場景下,其表現還略優於ParallelGC的並行Ful1GC實現。

即使是SerialGC,雖然比較古老,但是簡單的設計和實現未必就是過時的,它本身的開銷,不管是GC相關資料結構的開銷,還是執行緒的開銷,都是非常小的,所以隨著雲計算的興起,在serverless等新的應用場景下,Serial Gc找到了新的舞臺。

比較不幸的是CMSGC,因為其演算法的理論缺陷等原因,雖然現在還有非常大的使用者群體,但在JDK9中已經被標記為廢棄,並在JDK14版本中移除

Epsilon:A No-Op GarbageCollector(Epsilon垃圾回收器,”No-Op(無操作)“回收器)

ZGC:A Scalable Low-Latency Garbage Collector(Experimental)(ZGC:可伸縮的低延遲垃圾回收器,處於實驗性階段)

現在G1回收器已成為預設回收器好幾年了。我們還看到了引入了兩個新的收集器:ZGC(JDK11出現)和Shenandoah(Open JDK12)

ShenandoahGC: (主打特點:低停頓時間)

Shenandoah,無疑是眾多GC中最孤獨的一個。 是第一款不由oracle公司團隊領導開發的Hotspot垃圾收集器。不可避免的 受到官方的排擠。比如號稱openJDK和OracleJDk沒有區別的Oracle公司仍拒絕在oracleJDK12中支援Shenandoah。

Shenandoah垃圾回收器最初由RedHat進行的一項垃圾收集器研究專案Pauseless GC的實現,旨在針對JVM上的記憶體回收實現低停頓的需求。 在2014年貢獻給OpenJDK。

Red Hat研發Shenandoah團隊對外宣稱,Shenandoah垃圾回收器的暫停時間與堆大小無關,這意味著無論將堆設定為200MB還是200GB,99。9%的目標都可以把垃圾收集的停頓時間限制在十毫秒以內。 不過實際使用效能將取決於實際工作堆的大小和工作負載。

shenandoah GC的弱項:高執行負擔下的吞吐量下降。

shenandoah GC的強項:低延遲時間。

革命性的ZGC:

ZGC與shenandoah目標高度相似,在儘可能對吞吐量影響不大的前提下,實現在任意堆記憶體大小下都可以把垃圾收集的停頗時間限制在十毫秒以內的低延遲。

ZGC收集器是一款基於Region記憶體佈局的,(暫時)不設分代的,使用了讀屏障、染色指標和記憶體多重對映等技術來實現可併發的標記-壓縮演算法的,以低延遲為首要目標的一款垃圾收集器。

ZGC的工作過程可以分為4個階段:併發標記 - 併發預備重分配 - 併發重分配 - 併發重對映 等。

ZGC幾乎在所有地方併發執行的,除了初始標記的是STW的。所以停頓時間幾乎就耗費在初始標記上,這部分的實際時間是非常少的。

雖然ZGC還在試驗狀態,沒有完成所有特性,但此時效能已經相當亮眼,用“令人震驚、革命性”來形容,不為過。

未來將在服務端、大記憶體、低延遲應用的首選垃圾收集器。

今日份分享已結束,請大家多多包涵和指點!

相關文章