5 張圖帶你徹底理解 G1 垃圾收集器

作爲一款高效的垃圾收集器,G1 在 JDK7 中加入 JVM,在 JDK9 中取代 CMS 成爲了默認的垃圾收集器。

1 垃圾收集器回顧

1.1 新生代

新生代採用複製算法,主要的垃圾收集器有三個,Serial、Parallel New 和 Parallel Scavenge,特性如下:

1.1 老年代

CMS 缺點:吞吐量低、無法處理浮動垃圾、標記清除算法會產生大量內存碎片、併發模式失敗後會切到 Serial old。

2 G1 介紹

2.1 初識 G1

G1 垃圾收集器主要用於多處理器、大內存的場景,它有五個屬性:分代、增量、並行 (大多時候可以併發)、stop the word、標記整理。

      我們知道,垃圾收集器的一個目標就是 STW(stop the word) 越短越好。利用可預測停頓時間模型,G1 爲垃圾收集設定一個 STW 的目標時間 (通過 -XX:MaxGCPauseMillis 參數設定,默認 200ms),G1 儘可能地在這個時間內完成垃圾收集,並且在不需要額外配置的情況下實現高吞吐量。

G1 致力於在下面的應用和環境下尋找延遲和吞吐量的最佳平衡:

如果在 JDK8 中使用 G1,我們可以使用參數 -XX:+UseG1GC 來開啓。

G1 並不是一款實時收集器,它盡最大努力以高性能完成 MaxGCPauseMillis 設置的停頓時間,但並不能絕對保證在這個時間內完成收集。

2.2 堆佈局

G1 把整個堆分成了大小相等的 region,每一個 region 都是連續的虛擬內存,region 是內存分配和回收的基本單位。如下圖:

紅色帶 "S" 的 region 表示新生代的 survivor,紅色不帶 "S" 的表示新生代 eden,淺藍色不帶 "H" 的表示老年代,淺藍色帶 "H" 的表示老年代中的大對象。跟 G1 之前的內存分配策略不同的是,survivor、eden、老年代這些區域可能是不連續的。

G1 在停頓的時候可以回收整個新生代的 region,新生代 region 的對象要不復制到 survivor 區要不復制到老年代 region。同時每次停頓都可以回收一部分老年代的內存,把老年代從一個 region 複製到另一個 region。

2.3 關於 region

上一節我們看到,整個堆內存被 G1 分成了多個大小相等的 region,每個堆大約可以有 2048 個 region,每個 region 大小爲 1~32 MB(必須是 2 的次方)。region 的大小通過 -XX:G1HeapRegionSize 來設置,所以按照默認值來 G1 能管理的最大內存大約 32MB * 2048 = 64G。

2.4 大對象

大對象是指大小超過了 region 一半的對象,大對象可以橫跨多個 region,給大對象分配內存的時候會直接分配在老年代,並不會分配在 eden 區。

如下圖,一個大對象佔據了兩個半 region,給大對象分配內存時,必須從一個 region 開始分配連續的 region,在大對象被回收前,最後一個 region 不能被分配給其他對象。

大對象什麼時候回收?通常,只有在 mark 結束以後的 Cleanup 停頓階段或者 FullGC 的時候,死亡的大對象纔會被回收掉。但是,基本類型 (比如 bool 數組、所有的整形數組、浮點型數組等) 的數組大對象有個例外, G1 會在任何 GC 停頓的時候回收這些死亡大對象。這個默認是開啓的,但是可以使用 -XX:G1EagerReclaimHumongousObjects 這個參數禁用掉。

分配大對象的時候,因爲佔用空間太大,可能會過早發生 GC 停頓。G1 在每次分配大對象的時候都會去檢查當前堆內存佔用是否超過初始堆佔用閾值 IHOP(The Initiating Heap Occupancy Percent),如果當前的堆佔用率超過了 IHOP 閾值,就會立刻觸發 initial mark。關於 initial mark 詳見第 4 節

即使是在 FullGC 的時候,大對象也是永遠不會被移動的。這可能導致過早發生 FullGC 或者是意外的 OOM,因爲此時雖然還有大量的空閒內存,但是這些內存都是 region 中的內存碎片。

3 內存分配

G1 雖然把堆內存劃分成了多個 region,但是依然存在新生代和老年代的概念。G1 新增了 2 個控制新生代內存大小的參數,-XX:G1NewSizePercent(默認等於 5),-XX:G1MaxNewSizePercent(默認等於 60)。也就是說新生代大小默認佔整個堆內存的 5% ~ 60%。

根據前面介紹,一個堆大概可以分配 2048 個 region,每個 region 最大 32M,這樣 G1 管理的整個堆的大小最大可以是 64G,新生代佔用的大小範圍是 3.2G ~ 38.4G。

對於 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent,下面幾個問題需要注意:

  1. 如果設置了 - Xmn,那這兩個參數是否生效?

生效,比如堆大小是 64G,設置 -Xmn3.2G,那麼就等價於 -XX:G1NewSizePercent=5 並且 -XX:G1MaxNewSizePercent=5,因爲 3.2G/64G = 5%。

  1. 如果設置了 -XX:NewRatio,這兩個參數是否生效?

生效,比如堆大小是 64G,設置 -XX:NewRatio=3,那麼就等價於 -XX:G1NewSizePercent=25 並且 -XX:G1MaxNewSizePercent=25。因爲年輕代:老年代 = 1 :3,說明年輕代佔 1/4 = 25%。

  1. 如果 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 只設置其中一個,那這兩個參數還生效嗎?

設置的這個參數不生效,兩個參數都用默認值。

  1. 如果 - XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 這兩個參數都生效了,什麼時候動態擴容?

跟 -XX:GCTimeRatio 這個參數相關。這個參數爲 0~100 之間的整數 (G1 默認是 9, 其它收集器默認是 99),值爲 n 則系統將花費不超過 1/(1+n) 的時間用於垃圾收集。因此 G1 默認最多 10% 的時間用於垃圾收集,如果垃圾收集時間超過 10%,則觸發擴容。如果擴容失敗,則發起 Full GC。

4 垃圾回收

G1 的垃圾收集是在 Young-Only 和 Space-Reclamation 兩個階段交替執行的。如下圖:

young-only 階段會用對象逐步把老年代區域填滿,space-reclamation 階段除了會回收年輕代的內存以外,還會增量回收老年代的內存。完成後重新開始 young-only 階段。

4.1 Young-only

Young-only 階段流程如下圖:

這個階段從普通的 young-only GC 開始,young-only GC 把一些對象移動到老年代,當老年代的空間佔用達到 IHOP 時,G1 就停止普通的 young-only GC,開始初始標記 (Initial Mark)。

關於 IHOP,默認情況下,G1 會觀察標記週期內標記花了多少時間,老年代分配了多少內存,以此來自動確定一個最佳的 IHOP,這叫做自適應 IHOP。如果開啓這個功能,因爲初始時沒有足夠的觀察數據來確定 IHOP,G1 會用參數 -XX:InitiatingHeapOccupancyPercent 來指定初始 IHOP。可以用 -XX:-G1UseAdaptiveIHOP 參數關閉自適應 IHOP,這樣 IHOP 就參數 -XX:InitiatingHeapOccupancyPercent 指定的固定值。自適應 IHOP 這樣設置老年代佔有率, 當老年代佔有率 = 老年代最大佔有率 - 參數 -XX:G1HeapReservePercent 值時,啓動 space-reclamation 階段的第一個 Mixed GC。這裏參數 -XX:G1HeapReservePercent 作爲一個額外的緩存值。

關於標記,標記使用 SATB 算法,初始標記開始時,G1 保存堆的一份虛擬鏡像,這份鏡像存活的對象在後續的標記過程中也被認爲是存活的。這有一個問題,就是標記過程中如果部分對象死亡了,對於 space-reclamation 階段來說它們仍然是存活的 (也有少部分例外)。跟其他垃圾收集器相比,這會導致一部分死亡對象被錯誤保留,但是爲標記階段提供了更好的吞吐量,而且這些錯誤保留的對象會在下一次標記階段被回收。

在 young-only 階段,要回收新生代的 region。每一次 young-only 結束的時候,G1 總是會調整新生代大小。G1 可以使用參數 -XX:MaxGCPauseTimeMillis 和 -XX:PauseTimeIntervalMillis 來設置目標停頓時間,這兩個參數是對實際停頓時間的長期觀察得來的。他會根據在 GC 的時候要拷貝多少個對象,對象之間是如何相互關聯的等信息計算出來回收相同大小的新生代內存需要花費多少時間,

如果沒有其他的限定條件,G1 會把 young 區的大小調整爲 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 之間的值來滿足停頓時間的要求。

4.2 Space-reclamation

這個階段由多個 Mixed GC 組成,不光回收年輕代垃圾,也回收老年代垃圾。當 G1 發現回收更多的老年代區域不能釋放更多空閒空間時,這個階段結束。之後,週期性地再次開啓一個新的 Young-only 階段。

當 G1 收集存活對象信息時內存不足,G1 會做一個 Full GC,並且會 STW。

在 space-reclamation 階段,G1 會盡量在 GC 停頓時間內回收儘可能多的老年代內存。這個階段新生代內存大小被調整爲 -XX:G1NewSizePercent 設置的允許的最小值,只要存在可回收的老年代 region 就會被添加到回收集合中,直到再添加會超出目標停頓時間爲止。在特定的某個 GC 停頓時間內,G1 會按照這老年代 region 回收的效率 (效率高的優先收集) 和剩餘可用時間來得到最終待回收 region 集合。

每一個 GC 停頓期間要回收的老年代 region 數量受限於候選 region 集合數量除以 -XX:G1MixedGCCountTarget 這個參數值,參數 -XX:G1MixedGCCountTarget 指定一個週期內觸發 Mixed GC 最大次數,默認值 8。比如 -XX:G1MixedGCCountTarget 採用默認值 8,候選 region 集合有 200 個 region,那每次停頓期間收集 25 個 region。

候選 region 集合是老年代中所有佔用率低於 -XX:G1MixedGCLiveThresholdPercent 的 region。

當待回收 region 集合中可回收的空間佔用率低於參數值 -XX:G1HeapWastePercent 的時候,Space-Reclamation 結束。

4.3 內存緊張情況

當應用存活對象佔用了大量內存,以至於回收剩餘對象沒有足夠的空間拷貝時,就會觸發 evacuation failure。這時 G1 爲了完成當前的垃圾收集,會保留已經位於新的位置上的存活對象不動,對於沒有移動和拷貝的對象就不會進行拷貝了,僅僅調整對象間的引用。

evacuation failure 會導致一些額外的開銷,但是一般會跟其他 young GC 一樣快。evacuation failure 完成以後,G1 會跟正常情況下一樣繼續恢復應用的執行。G1 會假設 evacuation failure 是發生在 GC 的後期,這時大部分對象已經移動過了,並且已經有足夠的內存來繼續執行應用程序一直到 mark 結束 space-reclamation 開始。如果這個假設不成立 (也就是說沒有足夠的內存來執行應用程序),G1 最終只能發起 Full GC,對整個堆做壓縮,這個過程可能會非常慢。

5 跟其他收集器比較

5.1 Parallel GC

Parallel GC 可以壓縮和回收老年代的內存,但是也只能對老年代整體來操作。G1 以增量的方式把整個 GC 工作增量的分散到多個更短的停頓時間中,當然這可能會犧牲一定吞吐量。

5.2 CMS

跟 CMS 類似,G1 併發回收老年代內存,但是,CMS 採用標記 - 清除算法,不會處理老年代的內存碎片,最終就會導致長時間的 FullGC。

5.3 G1 問題

因爲採用併發收集,G1 的性能開銷會更大,這可能會影響吞吐量。

5.4 G1 優勢

G1 在任何的 GC 期間都可以回收老年代中全空或者佔用大空間的內存。這可以避免一些不必要的 GC,因爲可以非常輕易地釋放大量的內存空間。這個功能默認開啓,可以採用 -XX:-G1EagerReclaimHumongousObjects 參數關閉。

G1 可以選擇對整個堆裏面的 String 進行並行去重。這個功能默認關閉,可以使用參數 -XX:+G1EnableStringDeduplication 來開啓。

6 總結

本文詳細介紹了 G1 垃圾收集器,希望能夠對你理解 G1 有所幫助。

參考:

  1. https://docs.oracle.com/javase/10/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-CE6F94B6-71AF-45D5-829E-DEADD9BA929D

  2. https://mp.weixin.qq.com/s/KkA3c2_AX6feYPJRhnPOyQ

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2gatiH6wZyzuPO8vilHR8Q