關於GC原理和效能調優實踐,看這一篇就夠了!

零壹技術棧發表於2019-09-26

前言

本文介紹 GC 基礎原理和理論,GC 調優方法思路和方法,基於 Hotspot jdk1.8,學習之後你將瞭解如何對生產系統出現的 GC 問題進行排查解決。

關於GC原理和效能調優實踐,看這一篇就夠了!

正文

本文的內容主要如下:

  • GC 基礎原理,涉及調優目標,GC 事件分類、JVM 記憶體分配策略、GC 日誌分析等
  • CMS 原理及調優。
  • G1 原理及調優
  • GC 問題排查和解決思路

1. GC 基礎原理

1.1. GC 調優目標

大多數情況下對 Java 程式進行 GC 調優,主要關注兩個目標:

  • 響應速度(Responsiveness):響應速度指程式或系統對一個請求的響應有多迅速

    比如,使用者訂單查詢響應時間,對響應速度要求很高的系統,較大的停頓時間是不可接受的。調優的重點是在短的時間內快速響應。

  • 吞吐量(Throughput):吞吐量關注在一個特定時間段內應用系統的最大工作量

    例如每小時批處理系統能完成的任務數量,在吞吐量方面優化的系統,較長的 GC 停頓時間也是可以接受的,因為高吞吐量應用更關心的是如何儘可能快地完成整個任務,不考慮快速響應使用者請求

在 GC 調優中,GC 導致的應用暫停時間影響系統響應速度,GC 處理執行緒的 CPU 使用率影響系統吞吐量。

1.2. GC 分代收集演算法

現代的垃圾收集器基本都是採用分代收集演算法,其主要思想: 將 Java 的堆記憶體邏輯上分成兩塊:新生代、老年代,針對不同存活週期、不同大小的物件採取不同的垃圾回收策略。

關於GC原理和效能調優實踐,看這一篇就夠了!

1.2.1. 新生代(Young Generation)

新生代又叫年輕代,大多數物件在新生代中被建立,很多物件的生命週期很短。每次新生代的垃圾回收(又稱 Young GC、Minor GC、YGC)後只有少量物件存活,所以使用複製演算法,只需少量的複製操作成本就可以完成回收。

**新生代內又分三個區:**一個 Eden 區,兩個 Survivor 區(S0、S1,又稱From Survivor、To Survivor),大部分物件在 Eden 區中生成。

當 Eden 區滿時,還存活的物件將被複制到兩個 Survivor 區(中的一個);當這個 Survivor 區滿時,此區的存活且不滿足晉升到老年代條件的物件將被複制到另外一個 Survivor 區。物件每經歷一次複製,年齡加 1,達到晉升年齡閾值後,轉移到老年代。

1.2.2. 老年代(Old Generation)

在新生代中經歷了 N 次垃圾回收後仍然存活的物件,就會被放到老年代,該區域中物件存活率高。老年代的垃圾回收通常使用“標記-整理”演算法。

1.3. GC 事件分類

根據垃圾收集回收的區域不同,垃圾收集主要分為:

  • Young GC
  • Old GC
  • Full GC
  • Mixed GC

1.3.1. Young GC

新生代記憶體的垃圾收集事件稱為 Young GC(又稱 Minor GC),當 JVM 無法為新物件分配在新生代記憶體空間時總會觸發 Young GC。比如 Eden 區佔滿時,新物件分配頻率越高,Young GC 的頻率就越高。

Young GC 每次都會引起全線停頓(Stop-The-World),暫停所有的應用執行緒,停頓時間相對老年代 GC 造成的停頓,幾乎可以忽略不計。

1.3.2. Old GC/Full GC/Mixed GC

Old GC:只清理老年代空間的 GC 事件,只有 CMS 的併發收集是這個模式。

Full GC:清理整個堆的 GC 事件,包括新生代、老年代、元空間等 。

Mixed GC:清理整個新生代以及部分老年代的 GC,只有 G1 有這個模式。

1.4. GC 日誌分析

GC 日誌是一個很重要的工具,它準確記錄了每一次的 GC 的執行時間和執行結果,通過分析 GC 日誌可以調優堆設定和 GC 設定,或者改進應用程式的物件分配模式。

開啟的 JVM 啟動引數如下:

-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -XX:+PrintGCTimeStamps
複製程式碼

常見的 Young GC、Full GC 日誌含義如下:

  • Young GC

關於GC原理和效能調優實踐,看這一篇就夠了!

  • Full GC

關於GC原理和效能調優實踐,看這一篇就夠了!

免費的 GC 日誌圖形分析工具推薦下面 2 個:

  • GCViewer:下載 jar 包直接執行
  • gceasy:Web 工具,上傳 GC 日誌線上使用

1.5. 記憶體分配策略

Java 提供的自動記憶體管理,可以歸結為解決了物件的記憶體分配和回收的問題。前面已經介紹了記憶體回收,下面介紹幾條最普遍的記憶體分配策略:

1.5.1. 物件優先在 Eden 區分配

大多數情況下,物件在先新生代 Eden 區中分配。當 Eden 區沒有足夠空間進行分配時,虛擬機器將發起一次 Young GC。

1.5.2. 大物件直接進入老年代

JVM 提供了一個物件大小閾值引數(-XX:PretenureSizeThreshold,預設值為 0,代表不管多大都是先在 Eden 中分配記憶體)。

大於引數設定的閾值值的物件直接在老年代分配,這樣可以避免物件在 Eden 及兩個 Survivor 直接發生大記憶體複製。

1.5.3. 長期存活的物件進入老年代

物件每經歷一次垃圾回收,且沒被回收掉,它的年齡就增加 1,大於年齡閾值引數(-XX:MaxTenuringThreshold,預設 15)的物件,將晉升到老年代中。

1.5.4. 空間分配擔保

當進行 Young GC 之前,JVM 需要預估:老年代是否能夠容納 Young GC 後新生代晉升到老年代的存活物件,以確定是否需要提前觸發 GC 回收老年代空間,基於空間分配擔保策略來計算。

關於GC原理和效能調優實踐,看這一篇就夠了!

Young GC 之後如果成功(Young GC 後晉升物件能放入老年代),則代表擔保成功,不用再進行 Full GC,提高效能。

如果失敗,則會出現“promotion failed”錯誤,代表擔保失敗,需要進行 Full GC。

1.5.5. 動態年齡判定

新生代物件的年齡可能沒達到閾值(MaxTenuringThreshold 引數指定)就晉升老年代。

如果 Young GC 之後,新生代存活物件達到相同年齡所有物件大小的總和大於任意 Survivor 空間(S0+S1空間)的一半,此時 S0 或者 S1 區即將容納不了存活的新生代物件。

年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。

另外,如果 Young GC 後 S0 或 S1 區不足以容納:未達到晉升老年代條件的新生代存活物件,會導致這些存活物件直接進入老年代,需要儘量避免。

2. CMS 原理及調優

2.1. 術語解釋

2.1.1. 可達性分析演算法

用於判斷物件是否存活,基本思想是通過一系列稱為“GC Root”的物件作為起點(常見的 GC Root 有系統類載入器、棧中的物件、處於啟用狀態的執行緒等),基於物件引用關係,從 GC Roots 開始向下搜尋,所走過的路徑稱為引用鏈,當一個物件到 GC Root 沒有任何引用鏈相連,證明物件不再存活。

2.1.2. Stop The World

GC 過程中分析物件引用關係,為了保證分析結果的準確性,需要通過停頓所有 Java 執行執行緒,保證引用關係不再動態變化,該停頓事件稱為 Stop The World(STW)。

2.1.3. Safepoint

程式碼執行過程中的一些特殊位置,當執行緒執行到這些位置的時候,說明虛擬機器當前的狀態是安全的,如果有需要 GC,執行緒可以在這個位置暫停。

HotSpot 採用主動中斷的方式,讓執行執行緒在執行期輪詢是否需要暫停的標誌,若需要則中斷掛起。

2.2. CMS 演算法簡介

CMS(Concurrent Mark and Sweep 併發-標記-清除),是一款基於併發、使用標記清除演算法的垃圾回收演算法,只針對老年代進行垃圾回收。

CMS 收集器工作時,儘可能讓 GC 執行緒和使用者執行緒併發執行,以達到降低 STW 時間的目的。

通過以下命令列引數,啟用 CMS 垃圾收集器:

-XX:+UseConcMarkSweepGC
複製程式碼

值得補充的是,下面介紹到的 CMS GC 是指老年代的 GC,而 Full GC 指的是整個堆的 GC 事件,包括新生代、老年代、元空間等,兩者有所區分。

2.3. 新生代垃圾回收

能與 CMS 搭配使用的新生代垃圾收集器有 Serial 收集器和 ParNew 收集器。

這 2 個收集器都採用標記複製演算法,都會觸發 STW 事件,停止所有的應用執行緒。不同之處在於,Serial 是單執行緒執行,ParNew 是多執行緒執行。

關於GC原理和效能調優實踐,看這一篇就夠了!

2.4. 老年代垃圾回收

關於GC原理和效能調優實踐,看這一篇就夠了!

CMS GC 以獲取最小停頓時間為目的,儘可能減少 STW 時間,可以分為 7 個階段:

2.4.1. 初始標記(Initial Mark)

初始標記階段的目標是標記老年代中所有存活的物件, 包括 GC Root 的直接引用, 以及由新生代中存活物件所引用的物件,觸發第一次 STW 事件。

關於GC原理和效能調優實踐,看這一篇就夠了!

這個過程是支援多執行緒的(JDK7 之前單執行緒,JDK8 之後並行,可通過引數 CMSParallelInitialMarkEnabled 調整)。

2.4.2. 併發標記(Concurrent Mark)

併發標記階段 GC 執行緒和應用執行緒併發執行,遍歷階段 1 初始標記出來的存活物件,然後繼續遞迴標記這些物件可達的物件。

關於GC原理和效能調優實踐,看這一篇就夠了!

2.4.3. 併發預清理(Concurrent Preclean)

併發預清理階段 GC 執行緒和應用執行緒也是併發執行,因為階段 2 是與應用執行緒併發執行,可能有些引用關係已經發生改變。

關於GC原理和效能調優實踐,看這一篇就夠了!

通過卡片標記(Card Marking),提前把老年代空間邏輯劃分為相等大小的區域(Card)。

如果引用關係發生改變,JVM 會將發生改變的區域標記為 “髒區”(Dirty Card),然後在本階段,這些髒區會被找出來,重新整理引用關係,清除“髒區”標記。

2.4.4. 併發可取消的預清理(Concurrent Abortable Preclean)

併發可取消的預清理階段也不停止應用執行緒。本階段嘗試在 STW 的最終標記階段(Final Remark)之前儘可能地多做一些工作,以減少應用暫停時間。

在該階段不斷迴圈處理:標記老年代的可達物件、掃描處理 Dirty Card 區域中的物件,迴圈的終止條件有:

  • 達到迴圈次數
  • 達到迴圈執行時間閾值
  • 新生代記憶體使用率達到閾值

2.4.5. 最終標記(Final Remark)

這是 GC 事件中第二次(也是最後一次)STW 階段,目標是完成老年代中所有存活物件的標記,此階段會執行:

  • 遍歷新生代物件,重新標記
  • 根據 GC Roots,重新標記
  • 遍歷老年代的 Dirty Card,重新標記

2.4.6. 併發清除(Concurrent Sweep)

併發清除階段與應用程式併發執行,不需要 STW 停頓,根據標記結果清除垃圾物件。

關於GC原理和效能調優實踐,看這一篇就夠了!

2.4.7. 併發重置(Concurrent Reset)

併發重置階段與應用程式併發執行,重置 CMS 演算法相關的內部資料, 為下一次 GC 迴圈做準備。

2.5. CMS 常見問題

2.5.1. 最終標記階段停頓時間過長

CMS 的 GC 停頓時間約 80% 都在最終標記階段(Final Remark),若該階段停頓時間過長,常見原因是新生代對老年代的無效引用,在上一階段的併發可取消預清理階段中,執行閾值時間內未完成迴圈,來不及觸發 Young GC,清理這些無效引用。

通過新增引數:-XX:+CMSScavengeBeforeRemark。

在執行最終操作之前先觸發 Young GC,從而減少新生代對老年代的無效引用,降低最終標記階段的停頓。

但如果在上個階段(併發可取消的預清理)已觸發 Young GC,也會重複觸發 Young GC。

2.5.2. 併發模式失敗 & 晉升失敗

併發模式失敗:當 CMS 在執行回收時,新生代發生垃圾回收,同時老年代又沒有足夠的空間容納晉升的物件時,CMS 垃圾回收就會退化成單執行緒的 Full GC。所有的應用執行緒都會被暫停,老年代中所有的無效物件都被回收。

關於GC原理和效能調優實踐,看這一篇就夠了!

晉升失敗:當新生代發生垃圾回收,老年代有足夠的空間可以容納晉升的物件,但是由於空閒空間的碎片化,導致晉升失敗,此時會觸發單執行緒且帶壓縮動作的 Full GC。

關於GC原理和效能調優實踐,看這一篇就夠了!

併發模式失敗和晉升失敗都會導致長時間的停頓,常見解決思路如下:

  • 降低觸發 CMS GC 的閾值

    即引數 -XX:CMSInitiatingOccupancyFraction 的值,讓 CMS GC 儘早執行,以保證有足夠的空間

  • 增加 CMS 執行緒數,即引數 -XX:ConcGCThreads

  • 增大老年代空間

  • 讓物件儘量在新生代回收,避免進入老年代

2.5.3. 記憶體碎片問題

通常 CMS 的 GC 過程基於標記清除演算法,不帶壓縮動作,導致越來越多的記憶體碎片需要壓縮。

常見以下場景會觸發記憶體碎片壓縮:

  • 新生代 Young GC 出現新生代晉升擔保失敗(promotion failed))
  • 程式主動執行System.gc()

可通過引數 CMSFullGCsBeforeCompaction 的值,設定多少次 Full GC 觸發一次壓縮。

預設值為 0,代表每次進入 Full GC 都會觸發壓縮,帶壓縮動作的演算法為上面提到的單執行緒 Serial Old 演算法,暫停時間(STW)時間非常長,需要儘可能減少壓縮時間。

3. G1 原理及調優

3.1. G1 簡介

G1(Garbage-First)是一款面向伺服器的垃圾收集器,支援新生代和老年代空間的垃圾收集,主要針對配備多核處理器及大容量記憶體的機器。

G1 最主要的設計目標是:實現可預期及可配置的 STW 停頓時間。

3.2. G1 堆空間劃分

關於GC原理和效能調優實踐,看這一篇就夠了!

3.2.1. Region

為實現大記憶體空間的低停頓時間的回收,將劃分為多個大小相等的 Region。每個小堆區都可能是 Eden 區,Survivor 區或者 Old 區,但是在同一時刻只能屬於某個代。

在邏輯上, 所有的 Eden 區和 Survivor 區合起來就是新生代,所有的 Old 區合起來就是老年代,且新生代和老年代各自的記憶體 Region 區域由 G1 自動控制,不斷變動。

3.2.2. 巨型物件

當物件大小超過 Region 的一半,則認為是巨型物件(Humongous Object),直接被分配到老年代的巨型物件區(Humongous Regions)。

這些巨型區域是一個連續的區域集,每一個 Region 中最多有一個巨型物件,巨型物件可以佔多個 Region。

G1 把堆記憶體劃分成一個個 Region 的意義在於:

  • 每次 GC 不必都去處理整個堆空間,而是每次只處理一部分 Region,實現大容量記憶體的 GC。
  • 通過計算每個 Region 的回收價值,包括回收所需時間、可回收空間,在有限時間內儘可能回收更多的垃圾物件,把垃圾回收造成的停頓時間控制在預期配置的時間範圍內,這也是 G1 名稱的由來:Garbage-First。

3.3. G1 工作模式

針對新生代和老年代,G1 提供 2 種 GC 模式,Young GC 和 Mixed GC,兩種會導致 Stop The World。

3.3.1. Young GC

當新生代的空間不足時,G1 觸發 Young GC 回收新生代空間。

Young GC 主要是對 Eden 區進行 GC,它在 Eden 空間耗盡時觸發,基於分代回收思想和複製演算法,每次 Young GC 都會選定所有新生代的 Region。

同時計算下次 Young GC 所需的 Eden 區和 Survivor 區的空間,動態調整新生代所佔 Region 個數來控制 Young GC 開銷。

3.3.2. Mixed GC

當老年代空間達到閾值會觸發 Mixed GC,選定所有新生代裡的 Region,根據全域性併發標記階段(下面介紹到)統計得出收集收益高的若干老年代 Region。

在使用者指定的開銷目標範圍內,儘可能選擇收益高的老年代 Region 進行 GC,通過選擇哪些老年代 Region 和選擇多少 Region 來控制 Mixed GC 開銷。

3.4. 全域性併發標記

關於GC原理和效能調優實踐,看這一篇就夠了!

全域性併發標記主要是為 Mixed GC 計算找出回收收益較高的 Region 區域,具體分為 5 個階段:

3.4.1. 初始標記(Initial Mark)

暫停所有應用執行緒(STW),併發地進行標記從 GC Root 開始直接可達的物件(原生棧物件、全域性物件、JNI 物件)。

當達到觸發條件時,G1 並不會立即發起併發標記週期,而是等待下一次新生代收集,利用新生代收集的 STW 時間段,完成初始標記,這種方式稱為借道(Piggybacking)。

3.4.2. 根區域掃描(Root Region Scan)

在初始標記暫停結束後,新生代收集也完成的物件複製到 Survivor 的工作,應用執行緒開始活躍起來。

此時為了保證標記演算法的正確性,所有新複製到 Survivor 分割槽的物件,需要找出哪些物件存在對老年代物件的引用,把這些物件標記成根(Root)。

這個過程稱為根分割槽掃描(Root Region Scanning),同時掃描的 Suvivor 分割槽也被稱為根分割槽(Root Region)。

根分割槽掃描必須在下一次新生代垃圾收集啟動前完成(接下來併發標記的過程中,可能會被若干次新生代垃圾收集打斷),因為每次 GC 會產生新的存活物件集合。

3.4.3. 併發標記(Concurrent Marking)

標記執行緒與應用程式執行緒並行執行,標記各個堆中 Region 的存活物件資訊,這個步驟可能被新的 Young GC 打斷。

所有的標記任務必須在堆滿前就完成掃描,如果併發標記耗時很長,那麼有可能在併發標記過程中,又經歷了幾次新生代收集。

3.4.4. 再次標記(Remark)

和 CMS 類似暫停所有應用執行緒(STW),以完成標記過程短暫地停止應用執行緒, 標記在併發標記階段發生變化的物件,和所有未被標記的存活物件,同時完成存活資料計算。

3.4.5. 清理(Cleanup)

為即將到來的轉移階段做準備, 此階段也為下一次標記執行所有必需的整理計算工作:

  • 整理更新每個 Region 各自的 RSet(Remember Set,HashMap 結構,記錄有哪些老年代物件指向本 Region,key 為指向本 Region 的物件的引用,value 為指向本 Region 的具體 Card 區域,通過 RSet 可以確定 Region 中物件存活資訊,避免全堆掃描)
  • 回收不包含存活物件的 Region
  • 統計計算回收收益高(基於釋放空間和暫停目標)的老年代分割槽集合

3.5. G1調優注意點

3.5.1. Full GC 問題

G1 的正常處理流程中沒有 Full GC,只有在垃圾回收處理不過來(或者主動觸發)時才會出現,G1 的 Full GC 就是單執行緒執行的 Serial old gc,會導致非常長的 STW,是調優的重點,需要儘量避免 Full GC。

常見原因如下:

  • 程式主動執行 System.gc()
  • 全域性併發標記期間老年代空間被填滿(併發模式失敗)
  • Mixed GC 期間老年代空間被填滿(晉升失敗)
  • Young GC 時 Survivor 空間和老年代沒有足夠空間容納存活物件

類似 CMS,常見的解決是:

  • 增大 -XX:ConcGCThreads=n 選項增加併發標記執行緒的數量,或者 STW 期間並行執行緒的數量:-XX:ParallelGCThreads=n。
  • 減小 -XX:InitiatingHeapOccupancyPercent 提前啟動標記週期。
  • 增大預留記憶體 -XX:G1ReservePercent=n,預設值是 10,代表使用 10% 的堆記憶體為預留記憶體,當 Survivor 區域沒有足夠空間容納新晉升物件時會嘗試使用預留記憶體。

3.5.2. 巨型物件分配

巨型物件區中的每個 Region 中包含一個巨型物件,剩餘空間不再利用,導致空間碎片化,當 G1 沒有合適空間分配巨型物件時,G1 會啟動序列 Full GC 來釋放空間。

可以通過增加 -XX:G1HeapRegionSize 來增大 Region 大小,這樣一來,相當一部分的巨型物件就不再是巨型物件了,而是採用普通的分配方式。

3.5.3. 不要設定 Young 區的大小

原因是為了儘量滿足目標停頓時間,邏輯上的 Young 區會進行動態調整。如果設定了大小,則會覆蓋掉並且會禁用掉對停頓時間的控制。

3.5.4. 平均響應時間設定

使用應用的平均響應時間作為參考來設定 MaxGCPauseMillis,JVM 會盡量去滿足該條件,可能是 90% 的請求或者更多的響應時間在這之內, 但是並不代表是所有的請求都能滿足,平均響應時間設定過小會導致頻繁 GC。

4. 調優方法與思路

如何分析系統 JVM GC 執行狀況及合理優化?

GC 優化的核心思路在於,儘可能讓物件在新生代中分配和回收,儘量避免過多物件進入老年代,導致對老年代頻繁進行垃圾回收,同時給系統足夠的記憶體減少新生代垃圾回收次數,進行系統分析和優化也是圍繞著這個思路展開。

4.1. 分析系統的執行狀況

分析系統的執行狀況:

  • 系統每秒請求數、每個請求建立多少物件,佔用多少記憶體。
  • Young GC 觸發頻率、物件進入老年代的速率。
  • 老年代佔用記憶體、Full GC 觸發頻率、Full GC 觸發的原因、長時間 Full GC 的原因。

常用工具如下:

4.1.1. jstat

jstat 是 JVM 自帶命令列工具,可用於統計記憶體分配速率、GC 次數,GC 耗時。常用命令格式如下:

jstat -gc <pid> <統計間隔時間>  <統計次數>
複製程式碼

輸出返回值代表含義如下:

關於GC原理和效能調優實踐,看這一篇就夠了!

例如:jstat -gc 32683 1000 10,統計 pid=32683 的程式,每秒統計 1 次,統計 10 次。

4.1.2. jmap

jmap 也是 JVM 自帶命令列工具,可用於瞭解系統執行時的物件分佈。常用命令格式如下:

// 命令列輸出類名、類數量數量,類佔用記憶體大小,
// 按照類佔用記憶體大小降序排列
jmap -histo <pid>

// 生成堆記憶體轉儲快照,在當前目錄下匯出dump.hrpof的二進位制檔案,
// 可以用eclipse的MAT圖形化工具分析
jmap -dump:live,format=b,file=dump.hprof <pid>
複製程式碼

4.1.3. jinfo

用來檢視正在執行的 Java 應用程式的擴充套件引數,包括 Java System 屬性和 JVM 命令列引數。命令格式如下:

jinfo <pid> 
複製程式碼

4.1.4. 其他 GC 工具

  • 監控告警系統:Zabbix、Prometheus、Open-Falcon
  • jdk 自動實時記憶體監控工具:VisualVM
  • 堆外記憶體監控:Java VisualVM 安裝 Buffer Pools 外掛、google perf工具、Java NMT(Native Memory Tracking)工具
  • GC 日誌分析:GCViewer、gceasy
  • GC 引數檢查和優化:xxfox.perfma.com/

4.2. GC 優化案例

4.2.1. 資料分析平臺系統頻繁 Full GC

平臺主要對使用者在 App 中行為進行定時分析統計,並支援報表匯出,使用 CMS GC 演算法。

資料分析師在使用中發現系統頁面開啟經常卡頓,通過 jstat 命令發現系統每次 Young GC 後大約有 10% 的存活物件進入老年代。

原來是因為 Survivor 區空間設定過小,每次 Young GC 後存活物件在 Survivor 區域放不下,提前進入老年代。

通過調大 Survivor 區,使得 Survivor 區可以容納 Young GC 後存活物件,物件在 Survivor 區經歷多次 Young GC 達到年齡閾值才進入老年代。

調整之後每次 Young GC 後進入老年代的存活物件穩定執行時僅幾百 Kb,Full GC 頻率大大降低。

4.2.2. 業務對接閘道器 OOM

閘道器主要消費 Kafka 資料,進行資料處理計算然後轉發到另外的 Kafka 佇列,系統執行幾個小時候出現 OOM,重啟系統幾個小時之後又 OOM。

通過 jmap 匯出堆記憶體,在 eclipse MAT 工具分析才找出原因:程式碼中將某個業務 Kafka 的 topic 資料進行日誌非同步列印,該業務資料量較大,大量物件堆積在記憶體中等待被列印,導致 OOM。

4.2.3. 鑑權系統頻繁長時間 Full GC

系統對外提供各種賬號鑑權服務,使用時發現系統經常服務不可用,通過 Zabbix 的監控平臺監控發現系統頻繁發生長時間 Full GC,且觸發時老年代的堆記憶體通常並沒有佔滿,發現原來是業務程式碼中呼叫了 System.gc()。

小結

GC 問題可以說沒有捷徑,排查線上的效能問題本身就並不簡單,除了將本文介紹到的原理和工具融會貫通,還需要我們不斷去積累經驗,真正做到效能最優。

篇幅所限,不再展開介紹常見 GC 引數的使用,可以從 GitHub 克隆:

https://github.com/caison/caison-blog-demo
複製程式碼

參考

  • 《Java Performance: The Definitive Guide》 Scott Oaks
  • 《深入理解 Java 虛擬機器:JVM 高階特性與最佳實踐(第二版》 周志華
  • Java 效能調優實戰
  • Getting Started with the G1 Garbage Collector
  • GC 參考手冊-Java 版
  • 請教 G1 演算法的原理——RednaxelaFX 的回答
  • Java Hotspot G1 GC 的一些關鍵技術——美團技術團隊

轉載:陳彩華(caison),Akulaku 岩心科技開發工程師,喜歡研究分散式系統、線上問題排查、架構設計

關於公眾號

本帳號持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

關於GC原理和效能調優實踐,看這一篇就夠了!

相關文章