Java 垃圾回收算法之G1[精品長文]

願我所遇之人,所歷之事,哪怕由於我有一點點變好,我就心滿意足了。 html

G1(Garbage-First)回收器是在JDK1.7中正式使用的全新垃圾回收器,G1擁有獨特的垃圾回收策略,從分代上看,G1依然屬於分代垃圾回收器,它會區分年代和老年代,依然有eden和survivor區,但從堆的結構上看,它並不要求整個eden區、年清代或者老年代都連續。它使用了全新的分區算法。java

其特色以下:git

  • 並行性:G1在回收期間,能夠由多個GC線程同時工做,有效利用多核計算能力。github

  • 併發性:G1擁有與應用程序交替執行的能力,所以通常來講,不會在整個回收期間徹底阻塞應用程序。算法

  • 分代GC:與以前回收器不一樣,其餘回收器,它們要麼工做在年輕代要麼工做在老年代。G1能夠同時兼顧年輕代與老年代。性能優化

  • 空間整理:G1在回收過程當中,會進行適當的對象移動,不像CMS,只是簡單的標記清除,在若干次GC後CMS必須進行一次碎片整理,G1在每次回收時都會有效的複製對象,減小空間碎片。微信

  • 可預見性:因爲分區的緣由,G1能夠只選取部分區域進行內存回收,這樣縮小了回收範圍,所以對於全局停頓也能獲得更好的控制。併發

1、G1的內存劃分和主要收集過程

G1收集回收器將堆進行分區,劃分爲一個個的區域,每次收集的時候,只收集其中幾個區域,以此來控制垃圾回收產生一次停頓時間。oracle

G1的收集過程可能有4個階段:工具

  • 新生代GC

  • 併發標記週期

  • 混合收集

  • (若是須要)進行Full GC。

2、G1的新生代GC

新生代GC的主要工做是回收eden區和survivor區。

一旦eden區被佔滿,新生代GC就會啓動。新生代GC收集先後的堆數據以下圖所示,其中E表示eden區,S表示survivor區,O表示老年代。

能夠看到,新生代GC只處理eden和survivor區,回收後,全部的eden區都應該被清空,而survivor區會被收集一部分數據,可是應該至少仍然存在一個survivor區,類比其餘的新生代收集器,這一點彷佛並無太大變化。另外一個重要的變化是老年代的區域增多,由於部分survivor區或者eden區的對象可能會晉升到老年代。

3、G1併發標記週期

G1的併發階段和CMS有些相似,它們都是爲了下降一次停頓時間,而將能夠和應用程序併發執行的部分單獨提取出來執行。

併發標記週期針對老年代

併發標記週期可分爲如下幾步:

  • 初始標記:標記從根節點直接可達的對象。這個階段會伴隨一次新生代GC,它是會產生全局停頓的,應用程序在這個階段必須中止執行。

  • 根區域掃描:因爲初始標記必然會伴隨一次新生代GC,因此在初始化標記後,eden被清空,而且存活對象被移到survivor區。在這個階段,將掃描由survivor區直接可達的老年代區域,並標記這些直接可達的對象。這個過程是能夠和應用程序併發執行的。可是根區域掃描不能和新生代GC同時發生(由於根區域掃描依賴survivor區的對象,而新生代GC會修改這個區域),故若是恰巧此時須要新生代GC,GC就須要等待根區域掃描結束後才能進行,若是發生這種狀況,此次新生代GC的時間就會延長。

  • 併發標記:和CMS相似,併發標記將會掃描並查找整個堆的存活對象,並作好標記。這是一個併發過程,而且這個過程能夠被一次新生代GC打斷。

  • 從新標記:和CMS同樣,從新標記也是會使應用程序停頓,因爲在併發標記過程當中,應用程序依然運行,所以標記結果可能須要修正,因此在此階段對上一次標記進行補充。在G1中,這個過程使用SATB(Snapshot-At-The-Begining)算法完成,即G1會在標記之初爲存活對象建立一個快照,這個快照有助於加速從新標記的速度。

  • 獨佔清理:顧名思義,這個階段會引發停頓。它將計算各個區域的存活對象和GC回收比例並進行排序,識別可供混合回收的區域。在這個階段,還會更新記憶集。該階段給出了須要被混合回收的區域並進行了標記,在混合回收階段,須要這些信息。

  • 併發清理階段:識別並清理徹底空閒的區域。它是併發的清理,不會引發停頓。

SATB全稱是Snapshot-At-The-Beginning,由字面理解,是GC開始時活着的對象的一個快照。它是經過Root Tracing獲得的,做用是維持併發GC的正確性。那麼它是怎麼維持併發GC的正確性的呢?根據三色標記算法,咱們知道對象存在三種狀態:白:對象沒有被標記到,標記階段結束後,會被當作垃圾回收掉。灰:對象被標記了,可是它的field尚未被標記或標記完。黑:對象被標記了,且它的全部field也被標記完了。

SATB 利用 write barrier 將全部即將被刪除的引用關係的舊引用記錄下來,最後以這些舊引用爲根 Stop The World 地從新掃描一遍便可避免漏標問題。 所以G1 Remark階段 Stop The World 與 CMS了的remark有一個本質上的區別,那就是這個暫停只須要掃描有 write barrier 所追中對象爲根的對象, 而 CMS 的remark 須要從新掃描整個根集合,於是CMS remark有可能會很是慢。

4、混合回收

在併發標記週期中,雖有部分對象被回收,可是回收的比例是很是低的。可是在併發標記週期後,G1已經明確知道哪些區域含有比較多的垃圾對象,在混合回收階段,就能夠專門針對這些區域進行回收。固然G1會優先回收垃圾比例較高的區域(回收這些區域的性價比高),這正是G1名字的由來(Garbage First Garbage Collector:譯爲垃圾優先的垃圾回收器),這裏的垃圾優先(Garbage First)指的是回收時優先選取垃圾比例最高的區域。

這個階段叫作混合回收,是由於在這個階段,即會執行正常的年輕代GC,又會選取一些被標記的老年代區域進行回收,同時處理了新生代和老年代。

混合回收會被執行屢次,直到回收了足夠多的內存空間,而後,它會觸發一次新生代GC。新生代GC後,又可能會發生一次併發標記週期的處理,最後又會引發混合回收,所以整個過程多是以下圖:

5、必要時的Full GC

和CMS相似,併發收集讓應用程序和GC線程交替工做,所以在特別繁忙的狀況下無可避免的會發生回收過程當中內存不足的狀況,當遇到這種狀況,G1會轉入一個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。

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

解決這種問題的方式是:

  • 增長 -XX:G1ReservePercent選項的值(並相應增長總的堆大小),爲「目標空間」增長預留內存量。

  • 經過減小 -XX:InitiatingHeapOccupancyPercent 提早啓動標記週期。

  • 也能夠經過增長 -XX:ConcGCThreads 選項的值來增長並行標記線程的數目。

三、疏散失敗

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

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

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

解決這種問題的方式是:

  • 增長 -XX:G1ReservePercent選項的值(並相應增長總的堆大小),爲「目標空間」增長預留內存量。

  • 經過減小 -XX:InitiatingHeapOccupancyPercent 提早啓動標記週期。

  • 也能夠經過增長 -XX:ConcGCThreads 選項的值來增長並行標記線程的數目。

四、Humongous Object 分配失敗

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

對於Humongous Object 的處理還有一種方式就是切換GC算法到ZGC,由於ZGC中對於Humongous Object 的回收不會特殊處理(好比不會延遲收集)。

6、巨型對象

Humongous Object:巨型對象 Humongous regions:巨型區域

對於G1而言,只要超過regin大小的一半,就被認爲是巨型對象。巨型對象直接被分配到老年代中的「巨型區域」。這些巨型區域是一個連續的區域集。StartsHumongous 標記該連續集的開始,ContinuesHumongous 標記它的延續。

在分配巨型對象以前先檢查是否超過 initiating heap occupancy percent和the marking threshold, 若是超過的話,就啓動global concurrent marking,爲的是提前回收,防止 evacuation failures 和 Full GC。

對於巨型對象,有如下幾個點須要注意:

  • 沒有被引用的巨型對象會在標記清理階段或者Full GC時被釋放掉。

  • 爲了減小拷貝負載,只有在Full GC的時候,纔會壓縮大對象region。

  • 每個region中都只有一個巨型對象,該region剩餘的部分得不到利用,會致使堆碎片化。

  • 若是看到因爲大對象分配致使頻繁的併發回收,須要把大對象變爲普通的對象,建議增大Region size。(或者切換到ZGC)

對於增大Region size有一個負面影響就是:減小了可用region的數量。所以,對於這種狀況,你須要進行相應的測試,以查看是否實際提升了應用程序的吞吐量或延遲。

7、常見調優參數

一、-XX:MaxGCPauseMillis=N

默認200毫秒

前面介紹過使用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 左右。

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

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

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

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

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

8、細節

一、G1 mixed GC時機?

mixed gc中也有一個閾值參數 -XX:InitiatingHeapOccupancyPercent,當老年代大小佔整個堆大小百分比達到該閾值時,會觸發一次mixed gc.

在分配humongous object以前先檢查是否超過 initiating heap occupancy percent, 若是超過的話,就啓動global concurrent marking,爲的是提前回收,防止 evacuation failures 和 Full GC。

爲了減小連續H-objs分配對GC的影響,須要把大對象變爲普通的對象,建議增大Region size。

一個Region的大小能夠經過參數-XX:G1HeapRegionSize設定,取值範圍從1M到32M,且是2的指數。

二、XX:G1 HeapRegionSize 默認值?

默認把堆內存按照2048份均分,最後獲得一個合理的大小。

三、直接內存配置

Q: 何時用直接內存?

A: 讀寫頻繁的場合,出於性能考慮,能夠考慮使用直接內存。

直接內存也是 Java 程序中很是重要的組成部分,特別是 NIO 被普遍使用以後,直接內存能夠跳過 Java 堆,使 Java 程序能夠直接訪問原生堆空間。所以能夠在必定程度上加快內存的訪問速度。直接內存能夠用 -XX:MaxDirectMemorySize 設置,默認值爲最大堆空間,也就是 -Xmx。當直接內存達到最大值的時候,也會觸發垃圾回收,若是垃圾回收不能有效釋放空間,直接內存溢出依然會引發系統的 OOM。

通常而言直接內存在訪問讀寫上直接內存有較大優點(速度較快),可是在內存空間申請的時候,直接內存毫無優點而言。

四、RSet

全稱是Remembered Set,是輔助GC過程的一種結構,典型的空間換時間工具,和Card Table有些相似。G1的RSet是在Card Table的基礎上實現的:每一個Region會記錄下別的Region有指向本身的指針,並標記這些指針分別在哪些Card的範圍內。這個RSet實際上是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。

RSet到底是怎麼輔助GC的呢?

在作YGC的時候,只須要選定young generation region的RSet做爲根集,這些RSet記錄了old->young的跨代引用,避免了掃描整個old generation。而mixed gc的時候,old generation中記錄了old->old的RSet,young->old的引用由掃描所有young generation region獲得,這樣也不用掃描所有old generation region。因此RSet的引入大大減小了GC的工做量。

9、JDK 12中G1的新特性

一、可中斷 mixed GC

若是 Mixed GC 的 G1 存在超出暫停目標的可能性,則使其可被停止。

二、G1未使用分配內存即時返回

加強 G1垃圾收集器,以便在空閒時自動將 Java 堆內存返回給操做系統。

10、GC 發展趨勢

其實能夠看到Java 垃圾回收器的趨勢,就是在大內存堆的前提下盡 GC 可能的下降對應用程序的影響;從 CMS 的分階段增量標記,到 G1 經過 SATB 算法改正 remark 階段的 Stop The World 的影響,再到 ZGC/C4甚至在標記階段無需 Stop The World,莫不如此。

11、結尾

推薦幾種學習這種GC的方式:

  • 看JEP(JDK Enhancement Proposal)知道它的前因後果。

  • 看相應算法的paper(以前看Shenandoah GC Paper的時候,就有一種收穫很大的感受,由於Shenandoah GC的處理方式,介於G1跟ZGC之間,因此看了Shenandoah GC Paper感受對於G一、ZGC的理解也更加深刻了)。

會在文章結束,補充上JEP官網地址跟我收集的一些GC資料(包含部分paper)github地址。

補一個我本身概括的GC圖:

各類GC算法都是圍繞着,圖中內容展開的,只是各自的處理方式不一樣而已。

資料推薦:

一、GC算法及paper

github.com/jiankunking…

二、Java相關書籍推薦

github.com/jiankunking…

參考文獻

一、實戰JAVA虛擬機 JVM故障診斷與性能優化

二、jeps

三、其它

www.oracle.com/technetwork…

plumbr.io/handbook/gc…

我的微信公衆號:

我的CSDN博客:

blog.csdn.net/jiankunking

我的github:

github.com/jiankunking

相關文章
相關標籤/搜索