
阿里妹導讀
本文記錄了作者升級到JDK11後,使用G1GC導致記憶體利用率飆升至90%以上的問題及其解決方案。
背景
7 月份的時候,由於發現集團已經提供 JDK11 的流水線升級,可以透過流水線快速升級 JDK11,並解決相關的依賴問題。於是我歡天喜地地升級了 JDK11,在預發經過測試後沒有問題後,順利釋出上線,GC 次數有了明顯下降。
故障出現
線上穩定運行了半個月,突然開始觸發告警,記憶體利用率超過 85%。一看監控,發現出現了幾個現象。
1.記憶體利用率不斷升高,提升到 85%,一天內沒有下降。
2.G1 Old GC 不斷升高,沒有進行回收。


當時我設定的 JVM 引數非常簡單,只保留了 CMS 的堆記憶體設定,最大最小堆記憶體都是 12G。
-Xms12g -Xmx12g
透過 jmap 檢視 heap 情況(jhsdb jmap –heap –pid 185579):

臨時解決方案
1.保留一臺 beta 環境機器進行觀察,其他環境機器批次重啟。
2.重啟機器後,記憶體利用率下降,因此初步認定,造成告警的原因是 G1_OLD 區未觸發回收。

觀察情況
beta 機器在未重啟的情況下,記憶體使用率不斷提高,最後超過 90%(這也是本文標題由來)。


最終解決方案
經過查詢資料之後,最後的解決方案就是調整 JVM 引數,將堆記憶體縮減到8G,問題解決。
簡單結論:一般8G容器的堆記憶體建議設定4G,16G容器堆記憶體建議設定8G。
那麼從 JDK8 升級到 JDK11 之後,發生了什麼呢?為什麼同樣的引數在 CMS 上一點問題沒有,上到 G1GC 之後卻有這樣的問題呢?接下來我們好好介紹 G1GC。
從 GC 開始
記憶體中建立物件時發生了什麼?
記憶體分配的兩種手段
Java 最核心的一點特性就是它的記憶體管理機制。要聊 GC,我們先從記憶體分配開始。每個 Java 物件都需要在記憶體中有一個空間,用於儲存它的資料和引用關係。那麼建立物件時,怎麼在記憶體中劃出一份空間給物件呢?最直覺的方法跟陣列的使用是一致的。把記憶體認為是一個數組,用一個記憶體指標作為索引指向當前空閒空間的開頭,當需要為物件分配空間時,由於物件的大小是確定的,因此可以直接把指標後面的空間直接分給當前物件,只需要將指標移動指定大小的位置即可完成分配。這種方式就叫做指標碰撞。

指標碰撞:它透過維護一個指標,指向空閒記憶體的起始位置。當需要分配記憶體時,只需將該指標向前移動所需記憶體大小的距離即可。
透過這種技術進行記憶體分配後,當出現物件被回收的情況時,因為指標只有一個,因此無法把前面回收的部分進行再次分配,它只能繼續往後分配。因此又出現了一種新的技術。空閒列表技術。它的本質正如它的名字一樣,使用一個列表來記錄記憶體中的空閒位置,每次分配物件時,就會查詢這個列表,找到一塊大小可以分配當前物件的空閒空間,然後把物件放到這塊空間裡,再從空閒列表中把這塊空閒區域去掉。

空閒列表:它維護一個列表,記錄所有可用的記憶體塊資訊。當需要分配記憶體時,從列表中查詢合適大小的空閒塊進行分配。
多執行緒分配時怎麼辦?
以上兩種技術,遇到多執行緒時要怎麼處理呢?對於指標碰撞這種方案,最簡單的方式就是給每個執行緒劃分一個空間,每個執行緒在自己的空間中進行記憶體分配,這樣就不會發生衝突。這也就是 JVM 中 TLAB 的由來,(Thread Local Allocation Buffer)執行緒本地分配快取區,每次 GC 結束後,每個執行緒都會申請一塊 TLAB 進行記憶體分配,當 TLAB 申請不到時,執行緒會嘗試在 Eden 區分配,若 Eden 也不足,則可能觸發 Young GC。
而對於空閒列表,多執行緒的分配方案就是樂觀鎖機制,也就是CAS+失敗重試。

TLAB (Thread Local Allocation Buffer):執行緒本地分配快取區,每次 GC 結束後,每個執行緒都會申請一塊 TLAB 進行記憶體分配。
記憶體回收
記憶體回收的演算法
只有瞭解了記憶體的分配方式,我們才能更加深刻地理解記憶體回收的方案。針對前面我們說的兩種記憶體分配方式,我們可以提出以下這些記憶體回收的方案。
1.標記-清除演算法:標記出使用的物件,然後將沒有標記的物件清理。
-
缺點:隨著物件增多,標記和清理時間增加;會產生記憶體碎片。

2.標記-複製演算法:為了解決面對大量可回收物件時,清理時間增加的情況。出現了一種半區複製的演算法。也就是將記憶體分成兩部分,每次使用其中一部分,回收時,將少量剩餘的存活物件按順序複製到另一部分。
-
優點:執行簡單,回收效率高
-
缺點:記憶體只能使用一半,對記憶體是一種浪費。
-
後續改進:將記憶體分為三部分,伊甸園區、倖存者 1 區 、倖存者 2 區,也就是我們 GC 引數中常見的 8:1:1 的由來。

3.標記-整理:這個跟標記-複製有點像,核心差異點是,標記複製的物件移動是在兩個區來回移動的。而標記整理只會往記憶體的一端移動。
-
優點:沒有空間碎片
-
缺點:移動時需要暫停使用者程式,也就是觸發 STW

分代回收
分代回收,是指將記憶體區域分為新生代和老年代,不同代使用不同的回收手段。而這個分代回收是建立在兩條原則上的
1.絕大多數物件都是朝生夕死的。
2.熬過越多次垃圾收集過程的物件就越難以消亡。
如果一個區域中大多數物件都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的物件,就能以較低代價回收到大量的空,因此使用標記-複製演算法。
對於老年代的物件,由於他們難以使用消亡,頻繁的挪動成本非常的高,使用空閒列表的方式進行記憶體的分配和回收往往是價效比更高的方案。
CMS 回收就是使用標記清除演算法,標記垃圾,然後清除,再使用空閒列表記錄空閒區域。但是這樣,當記憶體碎片很多時,會存在總體空閒記憶體足夠多,但是卻不能給物件分配空間的情況。這個時候,CMS 就會使用標記-整理演算法,進行一次記憶體空間整理。
高吞吐和低延遲不能兼得
高吞吐(High Throughput)
高吞吐是指GC系統在單位時間內能夠處理的記憶體回收量。
低延遲(Low Latency)
低延遲是指垃圾回收過程中應用程式暫停的時間儘可能短。在低延遲的GC策略下,系統會盡量減少每次垃圾回收操作的停頓時間,即減少應用程式因等待GC完成而停止響應的時間。
不可兼得
高吞吐和低延遲之間存在一種權衡關係。通常情況下,為了實現高吞吐,GC可能會採取一些策略,比如增加垃圾回收的頻率或者擴大垃圾回收的範圍,這可能會導致每次垃圾回收的時間變長,從而增加了延遲。
相反,為了實現低延遲,GC可能會採取減少每次垃圾回收的範圍或者最佳化垃圾回收演算法來減少停頓時間,但這可能會增加垃圾回收的總次數,從而降低了吞吐量。
GC 策略轉變
回顧記憶體管理技術的發展歷程,GC 策略的演進呈現出明顯的方向性轉變:從追求高吞吐量逐步轉向對低延遲的最佳化,以適配現代網際網路服務的效能要求。早期的垃圾收集演算法以提升系統吞吐量為首要目標,其設計理念導致在極端場景下可能產生較高的延遲波動。隨著Java語言在網際網路服務領域的廣泛應用,加之硬體成本下降帶來的記憶體容量提升,系統架構逐漸能夠承載更頻繁的垃圾回收操作,以此換取更穩定的服務響應。
這一技術演變路徑在主流垃圾收集器的迭代中清晰可見:從採用併發標記清除(CMS)機制,到引入分代式收集器(G1),直至實現亞毫秒級停頓的ZGC無停頓收集器,每一種 GC 機制的出現都標誌著對對延遲控制的進一步最佳化。
CMS
CMS(Concurrent Mark Sweep)垃圾收集器是第一個關注 GC 停頓時間(STW 的時間)的垃圾收集器。
MS 就是 mark-sweep 的縮寫,即標記-清除演算法。
CMS 垃圾收集器之所以能夠實現對 GC 停頓時間的控制,其本質來源於對「可達性分析演算法」的改進,即三色標記演算法。在 CMS 出現之前,它們在進行垃圾回收的時候都需要 Stop the World,無法實現垃圾回收執行緒與使用者執行緒的併發執行。而 CMS 將整個回收過程分為了四步,在最耗時的標記和清除階段都可以跟使用者執行緒並行執行,這也是為什麼它延遲低的原因。
G1GC 介紹
G1GC 最大的特徵是非常重視即時性,G1GC 的即時性是軟即時性,即處理多用於稍微超出幾次最後期限也沒什麼問題的系統中,例如網路銀行系統。使用者總會期待所有的交易都能完美地處理好,但是稍微超出幾次最後期限,比如交易完成介面的展示慢了一些,應該也不會構成致命的問題。
為了實現軟即時性,它具備以下兩個功能。
1.設定期望暫停時間(最後期限)
2.可預測性
G1 如何實現可預測性
G1 會分步並行進行空間回收。G1 透過跟蹤先前應用行為和垃圾收集暫停的資訊,建立相關成本模型,從而實現可預測性。它利用這些資訊來確定暫停期間的工作量。
分割槽

G1 的老年代和年輕代不再是一塊連續的空間,整個堆被劃分成若干個大小相同的 Region,也就是區。
Region 的型別有 Eden、Survivor、Old、Humongous 四種,而且每個 Region 都可以單獨進行管理。
Humongous 是用來存放大物件的,如果一個物件的大小大於一個 Region 的 50%(預設值),那麼我們就認為這個物件是一個大物件。為了防止大物件的頻繁複製,我們可以將大物件直接放到 Humongous 中。
回收的實際過程
G1 回收的實際過程可以概括為一個迴圈,以下的官方文件的介紹。

On a high level, the G1 collector alternates between two phases. The young-only phase contains garbage collections that fill up the currently available memory with objects in the old generation gradually. The space-reclamation phase is where G1 reclaims space in the old generation incrementally, in addition to handling the young generation. Then the cycle restarts with a young-only phase.
翻譯:在高層次上,G1收集器在兩個階段之間交替進行。young only 階段包含了垃圾收集,這些收集逐漸用老年代中的物件填滿當前可用的記憶體。space-reclamation (空間回收)階段是G1在處理年輕代的同時,逐步回收老年代的空間。然後,週期以young only 階段重新開始。
怎麼理解這段話?
1.G1收集器的兩個主要階段:
-
僅年輕代階段(Young-only phase)
-
空間回收階段(Space-reclamation phase)
2.僅年輕代階段(Young-only phase):
-
這個階段主要進行年輕代的垃圾回收(Minor GC)。
-
存活物件可能會被提升到Survivor 區或老年代。
-
“逐漸用老年代物件填滿當前可用記憶體”的意思是:
-
新生代 GC 過程中,部分長生命週期的物件晉升到老年代。 -
經過多次 Minor GC,老年代會逐漸被這些晉升物件佔據。 -
“當前可用記憶體”指的是整個堆(Heap)中分配給老年代的部分。
3.空間回收階段(Space-reclamation phase):
-
這個階段進行Mixed GC(混合回收),同時清理年輕代和部分老年代。
-
目標是回收老年代中的垃圾物件,避免其佔滿整個堆。
-
由於 G1 採用Region(分割槽)管理,可以增量地清理老年代,不必一次性 STW 整個老年代。
-
這種增量老年代回收機制是 G1 相較於 CMS 的一大優勢。
4.迴圈過程:
-
G1 在 Young-only Phase 和 Space-reclamation Phase 之間交替執行。
-
在年輕代的物件逐漸晉升到老年代後,G1 進入空間回收階段,進行Mixed GC來回收老年代。
-
這一過程不斷迴圈,確保垃圾回收既高效又低延遲。
Young Only 介紹
-
這個階段主要進行 Minor GC(年輕代回收),存活物件晉升到老年代。
-
當老年代使用率達到InitiatingHeapOccupancyPercent (IHOP)閾值(稱為"初始堆佔用閾值"),G1 會啟動併發標記週期(Concurrent Marking Cycle),進入空間回收階段(Mixed GC 階段)的準備過程。
-
這一轉換的關鍵點是"併發開始"(Concurrent Start)GC,它是 Young GC但額外啟動了併發標記。
三個階段的介紹:
1.併發開始(Concurrent Start):
-
既執行普通的年輕代回收(Young GC),又啟動老年代的併發標記。
-
併發標記(Concurrent Marking)的作用是:
-
確定老年代中哪些物件是存活的,為後續的Mixed GC 選擇要清理的 Region。 -
在併發標記完成之前,普通的 Young GC 仍然會繼續發生。 -
標記階段的結束伴隨著STW 的 重新標記(Remark) 和 清理(Cleanup) 階段,確保準確性。
-
這是從 Young-only 階段向 Mixed GC 階段轉換的關鍵步驟。
2.重新標記(Remark):
-
用於完成併發標記的最終校正,需要Stop-The-World(STW)暫停
-
主要作用:
-
確保在最終標記時,所有存活物件的資訊是一致的(避免遺漏存活物件)。 -
計算Region 存活率,預估回收成本,預測 Mixed GC 的影響。 -
這一步暫停時間較短,因為 G1 採用了SATB(Snapshot-At-The-Beginning)作為可達性分析的基礎,減少了需要重新掃描的物件數量。
3.清理(Cleanup):
-
決定是否正式進入 Mixed GC(空間回收階段)。
-
關鍵點:
-
如果老年代的存活物件太多,G1 可能不會進入 Mixed GC,而是繼續 Young GC。 -
如果老年代垃圾足夠多,G1 進入 Mixed GC,並在下一次 Young GC 時,執行"Prepare Mixed"。 -
"Prepare Mixed" 這一步會標記哪些老年代的 Region 將參與下一次 GC,確保混合回收的效率。
4.總結:
僅年輕代階段(Young-only Phase)主要進行 Minor GC(Young GC),不斷將存活物件晉升到老年代。當老年代佔用率超過IHOP閾值時,G1 觸發併發開始 GC(Concurrent Start GC),啟動併發標記(Concurrent Marking),用於檢測老年代中的存活物件。併發標記完成後,G1 進入STW 的重新標記(Remark)和清理(Cleanup)階段,最終決定是否進入Mixed GC(空間回收階段)。如果老年代回收效率足夠高,G1 進入Mixed GC,否則繼續 Young GC。
Space-reclamation 介紹
混合回收(Mixed Collections, Mixed GC):這個階段,G1不僅清理年輕代,還會增量回收部分老年代,以防止老年代佔用率過高導致 Full GC。
這裡的關鍵點是“混合”:
1.繼續執行年輕代 GC(Minor GC)。
2.選取部分老年代 Region進行存活物件疏散(Evacuation),將存活物件複製到其他 Region,然後回收已清理的 Region。這樣,G1避免了 CMS 那種碎片化問題,確保老年代的可用空間更整齊。
具體流程:
1.Mixed GC 如何進行:
-
G1 會根據“老年代回收效率”選擇適合清理的老年代 Region,並按照回收價效比排序。
-
每次 Mixed GC 僅回收部分老年代 Region,並不會一次性清理整個老年代,以減少 STW(Stop-The-World)時間。
-
存活物件會被疏散(Evacuate)到 Survivor 區、老年代,甚至 Humongous 區(超大物件區),然後回收騰空的 Region。
-
這個過程可以併發執行,降低應用程式停頓時間。
2.結束條件:
-
G1 會持續執行 Mixed GC,直到“繼續清理老年代”變得不值得。
-
主要判斷標準:
-
回收的記憶體空間不足以抵消 GC 的成本。 -
透過引數G1MixedGCLiveThresholdPercent控制老年代存活率的閾值,當存活率過高時,不再清理這些 Region。 -
G1MixedGCCountTarget控制一次標記週期內 Mixed GC 的最大次數,超過後就結束。
3.迴圈重啟:
空間回收階段(Mixed GC)結束後,G1 重新進入 Young-only 階段。
這個過程不斷迴圈:Young-only Phase(僅年輕代階段) → Space-reclamation Phase(空間回收階段) → Young-only Phase
如果 Mixed GC 不能及時回收足夠的老年代空間,並且應用程式繼續分配物件導致堆空間耗盡,G1 會觸發 Full GC(STW 進行全堆壓縮整理)。Full GC 代價極高,一般是 G1 最不希望發生的情況。
4.總結:
空間回收階段(Space-reclamation Phase)主要透過Mixed GC(混合回收)逐步回收老年代。G1 選擇部分老年代 Region,將存活物件疏散到其他區域後釋放這些 Region,從而避免碎片化。Mixed GC 會持續進行,直到回收效率下降到設定閾值,之後進入新的 Young-only 階段。如果在這個過程中應用程式分配過快,G1 無法及時回收足夠的空間,就會觸發代價極高的Full GC。
G1 中的 GC 型別
G1 GC中涉及的幾種GC包括:
1.Young GC(新生代回收):
Young GC主要負責回收新生代中的物件。新生代包含新建立的物件,這些物件更有可能在短時間內變成垃圾。Young GC執行過程相對較快,因為它只涉及新生代中物件的掃描和回收。在Young GC過程中,Eden區和Survivor區的存活物件會被複制到另一個Survivor區或者晉升到老年代。這個過程是Stop-The-World(STW)的,意味著在回收過程中,應用程式的所有執行緒都會被暫停。
年輕代GC會選擇所有的年輕代區域加入回收集合中,但是為了滿足使用者停頓時間的配置,在每次GC後會調整這個最大年輕代區域的數量,每次回收的區域數量可能是變化的,換言之,young 區的大小是會動態調整的。
2.Mixed GC(混合回收):
Mixed GC是G1收集器特有的回收策略,它不僅回收新生代中的所有Region,還會回收部分老年代中的Region。這種策略的目標是在保證停頓時間不超過預期的情況下,儘可能地回收更多的垃圾物件。在Mixed GC過程中,首先會進行全域性併發標記(global concurrent marking),這個過程是併發的,與應用程式執行緒同時執行,用於標記出所有存活的物件。然後,在回收階段,G1會根據標記結果選擇收益較高的部分老年代Region和新生代Region一起進行回收。這個選擇過程是基於對Region中垃圾物件的數量和回收價值的評估。
3.Full GC:
Full GC是指對整個Java堆(包括年輕代、老年代和元空間)的垃圾回收。在G1中,通常儘量避免Full GC的發生,因為Full GC會導致較長時間的停頓。G1透過Mixed GC來回收部分老年代的Region,以減少Full GC的需要。但如果Mixed GC無法跟上記憶體分配的速度,導致老年代空間不足,或者在某些特殊情況下,比如巨型物件分配失敗時,就會觸發Full GC。G1中的Full GC使用的是Serial Old GC的程式碼,這意味著它會暫停所有應用執行緒,並執行效率相對較慢的單執行緒垃圾回收。它是原地(in-place)進行的,意味著在同一記憶體空間內移動和壓縮物件。
總結:G1 GC透過Young GC和Mixed GC來最佳化垃圾回收效能,減少停頓時間,而Full GC是作為最後的手段,在必要時對整個堆進行回收。
與 CMS 進行對比
CMS垃圾回收器是第一個併發垃圾回收器。像其他演算法一樣,CMS使用多個執行緒來執行回收操作。CMS垃圾回收器自JDK 11被正式廢棄,而且不鼓勵在JDK 8中使用。從實際的角度來看,由於CMS使用的是標記-清除演算法,導致它不能在後臺處理過程中壓縮堆。如果堆變得碎片化,那麼CMS必須停止所有的應用程式執行緒並壓縮堆,也就是執行標記-整理演算法,這就違背了使用併發垃圾回收器的初衷。因為這個原因,並且 G1 GC 具備自動整理堆記憶體的能力,減少了碎片問題,因此官方推薦使用 G1 GC 替代 CMS。
效能提升
整體而言 JDK11 優於 JDK8,G1 優於 CMS。在兩個 JDK 版本預設狀態下(JDK11 + G1 V.S JDK8 + CMS),JDK11 max-jOPS 分數提升 17%,critical-jOPS 分數提升 105%
GC
|
執行 SPECJbb2015 效能分析
|
|||
YGC平均暫停時間
|
吞吐量
|
max-jOPS
(純吞吐量)
|
critical-jOPS
(限制響應時間下的吞吐量)
|
|
JDK8+CMS(基線資料)
|
311ms
|
92.7%
|
15706
|
3898
|
JDK8+G1
|
187ms
|
94.7%
|
17098
|
5338
|
JDK11+CMS
|
274ms
|
94.2%
|
15905
|
4821
|
JDK11+G1
|
177ms (↓43%)
|
94.7%(↑2pt)
|
18376 (↑17%)
|
7980(↑105%)
|
資料來自:AArch64版JDK8/11效能分析
記憶體使用率增加的覆盤
記憶體分配原理
物理記憶體指的是PSS或RSS, 包括堆內的物理記憶體和堆外的, 小於等於預留的記憶體. 物理記憶體的分配方式通常是按需分配, 也就是隻有寫入虛擬記憶體的時候,核心才進行物理記憶體分配 (又叫touch)。讀取虛擬記憶體或者預留 (mmap) 虛擬記憶體, 程序當時都不會產生物理記憶體。
換言之,透過JVM的引數-Xmx和-Xms可以設定JVM的堆大小,但是此時作業系統分配的只是虛擬記憶體,只有JVM真正要使用該記憶體時,才會被分配物理記憶體。
記憶體分配過程
1.物件首先會先分配在年輕代,因為之前分配的只是虛擬記憶體,所以每次新建物件都需要作業系統來先分配物理記憶體,只有等第一次新生代GC後,該被分配的記憶體空間都已經分配了,之後分配物件的速度才會加快。
2.那麼老年代也是同理,老年代的空間何時真正使用,自然是物件需要晉升到老年代時,所以新生代GC的時候,物件要從新生代晉升到老年代,作業系統也需要為老年代先分配物理記憶體。
為什麼升級後記憶體佔用率提升
不同分割槽策略導致的
G1老區佔用比CMS多, 原因是
1.CMS 採用的是 按地址順序分配老年代記憶體,並且固定老年代的回收閾值。在低負載下,老年代記憶體的某些區域可能長時間不會被觸及,造成碎片化,直到記憶體壓力較高時才開始回收。
2.G1 GC使用Region-based 分配策略,這種策略採用了啟發式演算法,嘗試儘量填滿堆記憶體,避免老年代有空閒區域而造成記憶體浪費。因此,G1 的區域分配比 CMS 更加靈活,它會自動調整閾值並較快touch所有堆記憶體。
記憶集的佔用
CMS 中有一個結構叫 card table,卡表,它是一種記錄老年代對新生代引用的機制,卡表是為了解決了在新生代垃圾收集時,如何快速找到老年代中引用新生代物件的問題。它的工作原理:將老年代記憶體空間劃分為固定大小的卡頁。使用一個位元組陣列(卡表)來標記每個卡頁的狀態。如果一個卡頁中的物件可能引用了新生代物件,該卡頁就被標記為"髒"。在進行新生代垃圾收集時,只需要掃描被標記為髒的卡頁,而不是整個老年代。
像 G1 這種分割槽回收演算法,有些 Region 可能被選入 CSet,有些則不會。所以,我們需要知道當一個 Region 需要被回收時,有哪些其他的 Region 引用了自己。相應地,為了加快定位速度,分割槽回收演算法為每個 Region 都引入了記錄集(Remembered Set,RSet),每個 Region 都有自己的專屬 RSet。和 Card table 不同的是,RSet 記錄誰引用了我,這種記錄集被人們稱為 point-in 型的,而 Card table 則記錄我引用了誰,這種記錄集被稱為 point-out 型。
1.不需要記錄的引用:
-
同一個Region內的引用:因為它們都在同一個區域內,所以不需要在RSet中記錄。
-
年輕代Region到其他Region的引用:在垃圾回收時,年輕代的所有Region都會被清理,這意味著所有年輕代物件都會被檢查,因此不需要在RSet中記錄這些引用。
-
CSet集合的Region到其他Region的引用:CSet是即將被回收的Region集合,這些Region中的物件也會被完全檢查,所以同樣不需要記錄。
2.需要記錄的引用:
-
非CSet老年代Region到年輕代Region的引用:這些引用需要被記錄,以便在回收年輕代時知道哪些老年代物件正在引用年輕代物件。
-
非CSet老年代Region到CSet老年代Region的引用:這些引用也需要被記錄,以便在回收CSet中的老年代Region時,知道哪些其他老年代物件還在引用它們。
3.記錄引用的方式:透過寫屏障(Write Barrier)機制,當物件A在Region1中引用物件B在Region2時,這個引用資訊(稱為“dirty card”)會被新增到Region2的RSet中。在 G1 中,我們把這種 card 稱為 dirty card。業務執行緒也不是直接將 dirty card 放到 RSet 中的。而是在業務執行緒中引入一個叫做 dirty card queue(DCQ)的佇列,在寫屏障中,業務執行緒只需要將 dirty card 放入 DCQ 中。接下來, G1 GC 中的 Refine 執行緒,會從 DCQ 中找到這種 dirty card,然後再去做更精細的檢查,只有確實不屬於上面所描述的三種情況的跨區引用,才真正放到專屬 RSet 中去。
值得一提的是,Java 一直在嘗試最佳化記憶集的佔用,下圖是 P99 CONF-G1:To Infinity and Beyond 的一個對比,在 16G 的堆記憶體情況下,高版本的的 JDK 的記憶集大小有了明顯的改善。

總結
1.由於設定了 12G 的堆記憶體,且 G1 修改了記憶體管理方式,導致 G1 能夠佔用滿這 12G 的堆記憶體,而 CMS 由於有固定old區回收閾值, 如果低壓力下, old區末尾的部分記憶體幾乎永遠不會被用到,因此即使分配了 12G 記憶體也不會真正佔用 12G 記憶體。
2.JDK11 下的 G1 的記憶集佔用了較大的空間。
因此,12g 堆記憶體+2.25g記憶集+700m 的全域性記憶體+非堆記憶體 ≈ 16g,這也是為什麼最後能升到 95% 以上的原因。
幾種解決方案
啟動時 touch
如果想要避免記憶體逐漸增加並導致突發的延遲,JVM 提供了-XX:+AlwaysPreTouch 引數。該引數能夠在服務啟動時預先將虛擬記憶體對映為物理記憶體,避免後續因按需分配物理記憶體而導致的延遲。這樣可以提高程式在執行時的記憶體分配效率,避免由於記憶體分配引起的卡頓或停頓。但它的缺點是會顯著增加 JVM 啟動時間,畢竟把分配物理記憶體的事提前放到JVM程序啟動時做了,尤其是在記憶體較大的情況下,啟動時間可能會大幅增加。
縮小堆記憶體
雖然在 G1 下老年代記憶體使用較高,但通常不會引發 OOM(記憶體溢位)風險,因為即使在長時間執行中,G1 也會調整回收策略以避免記憶體溢位。如果記憶體使用率已經達到警戒線,建議適當縮小堆大小。通常認為在 JDK11中,對於許多 Java 應用而言,堆記憶體佔總記憶體的一半是比較合適的選擇。
G1GC引數調優
CMS切換成G1的引數替換
CMS原引數
|
G1替換引數 (加粗為必改, 否則選改)
|
解釋
|
-Xms10g -Xmx10g
|
-Xms10g-Xmx10g 或
-Xms9500m -Xmx9500m
|
G1堆外記憶體稍高, 且使用region-based的管理演算法, 導致footprint稍高, 為了不出現意外容器OOM, 適當降低5-8%的堆大小
比如buy2本來記憶體已經到82%了, 再高就要破90%了, 需要調小以應對風險
注: 記憶體水線是視業務行為而定的, 比如ump2不用調堆, 也可以保持原來的水線
注2: 這個幅度調小堆一般不會導致Java OOM, 因為G1對Survivor區的利用率比CMS高不少, 碎片化風險也比CMS低
|
-Xmn6g
|
-XX:MaxNewSize=6g
|
把-Xmn替換成-XX:MaxNewSize即可, 數字不變
也可以省略這個引數, 因為預設MaxNewSize就是60%, 所以相當於10g x 60% = 6g, 但是必須要記得把Xmn刪掉
總之G1需要靈活調整Young區, 所以G1不要設定Xmn
|
-XX:G1HeapRegionSize=32m
|
G1跟CMS的一個巨大差別就是他用region-based的管理演算法, 超過region大小的一半會當成"大物件", 大物件太多了有損效能,原因是 G1 對於大物件的處理非常消極,基本上不會怎麼處理
|
|
-XX:+CMSScavengeBeforeRemark
-XX:+UseConcMarkSweepGC
-XX:CMSMaxAbortablePrecleanTime=5000
-XX:+CMSClassUnloadingEnabled
|
-XX:+UseG1GC
|
CMS相關引數, 不需要
|
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
|
-XX:InitiatingHeapOccupancyPercent=40
-XX:-G1UseAdaptiveIHOP
|
控制Old區大小, 比如原來CMS大約在 (0.8*(10g-6g)=3277m) 觸發Old區GC, G1配置成在 (0.4*9500=3800m) 觸發
因為G1會把大物件也往老區放, 所以我們會傾向於把這個設定得比CMS高一點
|
-XX:G1HeapWastePercent=2
|
G1預設會接受5%的"浪費", 把他減少到2%, 可以大約擠出200-300m的空間, 當然這個是有代價的, G1回收成本會變高, 視情況加
|
推薦引數介紹
let JVM_HEAP = "-Xms8g -Xmx8g -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=512m -XX:MaxDirectMemorySize=512m"
let JVM_GC = "-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=40 -XX:-G1UseAdaptiveIHOP -XX:+ExplicitGCInvokesConcurrent -XX:ParallelGCThreads=8 -Dio.netty.tryReflectionSetAccessible=true --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED"
-XX:MaxGCPauseMillis
設定期望的最大GC停頓時間指標,G1收集器會盡力在這個時間內完成垃圾回收,以減少應用程式的停頓時間。預設值是200毫秒,調整這個引數可以影響GC的停頓時間
無論是 young GC 還是 mixed GC,都會回收全部的年輕代,因此這個引數如果設定得太短,會限制 young 區佔用的 region數量,可能會導致G1跟不上垃圾產生的速度,最終退化成Full GC。
-XX:ReservedCodeCacheSize
作用:
-
儲存編譯後的原生代碼:當 JVM 執行時,它會將頻繁執行的 Java 位元組碼編譯成本地機器程式碼以提高效能。這些編譯後的程式碼被儲存在程式碼快取中。
-
影響效能:適當大小的程式碼快取可以提高應用程式的效能,因為它允許更多的程式碼被編譯和最佳化。
-XX:MaxDirectMemorySize
定義:MaxDirectMemorySize 指定了 JVM 可以分配的最大直接記憶體大小。
直接記憶體(Direct Memory):
-
這是在 Java 堆外分配的記憶體,不受 Java 堆大小限制。
-
主要用於 NIO(New I/O)操作,如 DirectByteBuffer。
-
可以減少垃圾回收的壓力,因為它不在 Java 堆中。
預設值:如果不指定,預設值通常等於 -Xmx(最大堆大小)。
-XX:G1HeapRegionSize
設定每個Region的大小,G1的目標是根據最小的Java堆大小劃分出約2048個這樣的區域。這個值必須是2的冪,範圍在1MB到32MB之間。預設情況下,這個值是堆記憶體的1/2000,這意味著G1收集器管理的最小堆記憶體應該是2GB以上,最大堆記憶體為64GB。調整這個引數可以影響G1的記憶體管理效率和垃圾回收的效能。
-XX:+ExplicitGCInvokesConcurrent
用於控制顯式垃圾回收(Explicit GC)呼叫的行為。當這個引數被啟用時,它會使得透過 System.gc() 或者其他形式的顯式垃圾回收請求觸發併發(concurrent)的垃圾回收操作,而不是完全停止(full stop-the-world)的應用程式執行緒進行垃圾回收。
-XX:ParallelGCThreads
1.定義:-XX:ParallelGCThreads 用於設定並行垃圾收集器的執行緒數。這些執行緒用於執行 Stop-The-World(STW)垃圾收集階段。
2.預設值:
-
如果 CPU 核心數小於等於 8:ParallelGCThreads = CPU 核心數
-
如果 CPU 核心數大於 8:ParallelGCThreads = 8 + ((CPU 核心數 – 8) * 5/8)
-XX:G1NewSizePercent & -XX:G1MaxNewSizePercent
分別用於設定新生代最小和最大容量的百分比。這兩個引數可以控制新生代的大小,影響垃圾收集的頻率和效能。一般不推薦修改。
IHOP
The Initiating Heap Occupancy Percent (IHOP) is the threshold at which an Initial Mark collection is triggered and it is defined as a percentage of the old generation size.G1 by default automatically determines an optimal IHOP by observing how long marking takes and how much memory is typically allocated in the old generation during marking cycles. This feature is called Adaptive IHOP. If this feature is active, then the option -XX:InitiatingHeapOccupancyPercent determines the initial value as a percentage of the size of the current old generation as long as there aren't enough observations to make a good prediction of the Initiating Heap Occupancy threshold. Turn off this behavior of G1 using the option-XX:-G1UseAdaptiveIHOP. In this case, the value of -XX:InitiatingHeapOccupancyPercent always determines this threshold.Internally, Adaptive IHOP tries to set the Initiating Heap Occupancy so that the first mixed garbage collection of the space-reclamation phase starts when the old generation occupancy is at a current maximum old generation size minus the value of -XX:G1HeapReservePercent as the extra buffer.
翻譯:初始堆佔用百分比(IHOP)是觸發初始標記收集的閾值,它被定義為老年代大小的百分比。G1預設會透過觀察標記過程所需的時間以及在標記週期期間通常在老年代中分配的記憶體量來自動確定最佳的IHOP。這個特性被稱為自適應IHOP。如果這個特性是啟用的,那麼 -XX:InitiatingHeapOccupancyPercent 選項會決定初始值,作為當前老年代大小的百分比,這種情況會持續到有足夠的觀察資料來做出良好的初始堆佔用閾值預測為止。使用 -XX:-G1UseAdaptiveIHOP 選項可以關閉G1的這種行為。在這種情況下,-XX:InitiatingHeapOccupancyPercent 的值將始終決定這個閾值。在內部,自適應IHOP試圖設定初始堆佔用,使得空間回收階段的第一次混合垃圾收集在老年代佔用達到當前最大老年代大小減去 -XX:G1HeapReservePercent 值作為額外緩衝區時開始。
1.自適應IHOP:
-
G1預設使用自適應IHOP機制。
-
它透過觀察標記過程的時間和老年代記憶體分配情況來動態調整IHOP。
-
目的是找到最優的IHOP值,以平衡垃圾收集頻率和效率。
2.-XX:InitiatingHeapOccupancyPercent:
-
這個引數設定IHOP的初始值。
-
在自適應IHOP有足夠資料之前,這個值會被使用。
-
如果停用自適應IHOP,這個值將始終決定IHOP閾值。
3.停用自適應IHOP:
-
使用 -XX:-G1UseAdaptiveIHOP 可以關閉自適應IHOP。
-
關閉後,IHOP將固定為 -XX:InitiatingHeapOccupancyPercent 指定的值。
4.自適應IHOP的內部工作:
-
目標是在老年代佔用達到特定水平時開始混合收集。
-
這個水平是:當前最大老年代大小 – G1HeapReservePercent。
-
G1HeapReservePercent 作為一個額外的緩衝區。
-XX:InitiatingHeapOccupancyPercent
這個引數指定觸發在老年代的記憶體空間達到一定百分比之後,啟動併發標記。預設值是 45%,當然,這更進一步是為了觸發 mixed GC,以此來回收老年代。如果一個應用老年代物件產生速度較快,可以嘗試適當調小 IHOP。
1.併發標記的目的:
-
併發標記的主要目的是為後續的 Mixed GC 做準備,識別整個堆中的存活物件。
-
Mixed GC 會同時收集年輕代和部分老年代。
2.G1 收集器的工作流程:
-
通常順序是:若干次 Young GC → 併發標記 → Mixed GC
-
併發標記後,G1 可能會執行一系列 Mixed GC 來回收標記的區域。
-XX:-G1UseAdaptiveIHOP
UseAdaptiveIHOP 是 G1 垃圾收集器的一個功能,它根據堆記憶體晉升速率自動調節 IHOP(Initiating Heap Occupancy Percent)。IHOP 決定了觸發併發標記週期的堆佔用閾值。使用 -XX:-G1UseAdaptiveIHOP 引數可以停用這個自動調節功能,此時 IHOP 值將固定為初始設定,通常由 -XX:InitiatingHeapOccupancyPercent 引數指定。這種做法類似於使用 -Xmn 引數固定年輕代大小。
關於是否應該停用 UseAdaptiveIHOP,存在不同觀點。有建議關閉此功能以防止頻繁的併發 GC,而另一種觀點認為除非 G1 的自動調節表現不佳,否則不應更改。
我的看法是,如果應用程式在預設設定下表現良好,就保持 UseAdaptiveIHOP 啟用。只有在遇到明確的效能問題時,才考慮停用它。在做出任何更改之前,最好先收集詳細的 GC 日誌進行分析,然後根據分析結果決定是否需要調整。需要注意的是,更改 IHOP 設定可能會影響 GC 的頻率和停頓時間,因此調整後需要進行充分的測試和監控。
-XX:G1HeapWastePercent
1.引數定義:-XX:G1HeapWastePercent 設定了 G1 垃圾收集器在停止回收之前允許的堆記憶體浪費百分比。
2.預設值:預設值是 5%,意味著 G1 允許最多 5% 的堆記憶體被浪費(即未被使用)。
3.引數作用:
-
控制空間回收階段(Space-reclamation phase)的結束時機。
-
影響混合收集(Mixed Collections)的次數。
4.工作原理:
-
在空間回收階段,G1 執行混合收集,回收老年代和年輕代的區域。
-
G1 會持續進行混合收集,直到堆記憶體中的自由空間(未使用空間)百分比達到 100% – G1HeapWastePercent。
-
一旦達到這個閾值,G1 認為繼續回收的收益不大,就會停止當前的空間回收階段。
5.調優考慮:
-
降低這個值會導致 G1 執行更多的混合收集,可能會增加總的 GC 時間,但可以得到更緊湊的堆。
-
提高這個值會減少混合收集的次數,可能會減少 GC 時間,但可能會留下更多未使用的空間。
6.使用場景:
-
對於記憶體敏感的應用,可以考慮降低這個值,以更充分地利用堆空間。
-
對於更關注吞吐量的應用,可以考慮略微提高這個值,以減少 GC 的頻率。
netty 最佳化
-Dio.netty.tryReflectionSetAccessible=true --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED
必須設定,解決堆外記憶體增加 600M 的問題。
參考資料:
1.G1GC官方介紹:Garbage-First Garbage Collector
https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-g1-garbage-collector1.html#GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573
2.G1GC官方調優推薦:Garbage-First Garbage Collector Tuning
https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector-tuning.html
3.深入理解Java虛擬機器(第3版):https://book.douban.com/subject/34907497/
4.Java 效能權威指南:https://www.ituring.com.cn/book/2774
5.P99 CONF-G1:To Infinity and Beyond:https://www.p99conf.io/session/g1-to-infinity-and-beyond/
基於快取實現應用提速
隨著業務發展,承載業務的應用將會面臨更大的流量壓力,如何降低系統的響應時間,提升系統性能成為了每一位開發人員需要面臨的問題,使用快取是首選方案。本方案介紹如何運用雲資料庫Redis版構建快取為應用提速。
點選閱讀原文檢視詳情。