記一次jvm調優及垃圾收集器

KerryWu發表於2023-03-31

本文在第一段先簡單講解調優的緣由和過程,具體涉及到的知識點,在後面段中具體介紹。

1. 調優過程

1.1. 問題定位

有一天突然收到監控告警,大批次產線服務例項在自動重啟。於是趕緊上平臺下載dump日誌,以及檢查其他監控事件,最終定位到問題:

那幾分鐘內,湧入幾十萬使用者登入平臺操作,導致記憶體吃緊,幾乎每個例項都觸發了幾次 Full GC。而由於集中性的 Full GC,STW 時間過長,服務測活介面長期調不通,k8s判定服務故障,就重啟pod。

問題定位了,除了最佳化程式碼,減少無效記憶體大量佔用以外,還可以調優一下Jvm引數了。

1.2. gc 問題定位

既然是 gc 出的問題,那就透過 jstat -gcutil pid 時長間隔 命令,實時看一下gc的過程狀態。

透過一段時間觀察,發現每次 young gc 後,survivor 區域中佔用比例很高(近百分之百),甚至某些次 old 區域中有略微增長。這說明一個問題:

young gc 後存活的物件太多,survivor區存放不下,溢位的物件就直接進入了老年代。這就加快了老年代記憶體的佔用速度,提前需要 full gc。

gc 的問題也定位到了,接下來分幾個步驟最佳化

1.3. gc 最佳化

分了幾個維度:

  • 最直觀表現是 survivor 區不足,因此可以加大一下年輕代中 survivor 比例,即減少-XX:SurvivorRatio(eden區和單個survivor區的比例,預設值:8)的值。
  • 稍微加大一下年輕代的佔比,即減少-XX:NewRatio(老年代和年輕代的比例,預設值:2)的值。
  • 最根本的還是記憶體不足,所以如果可以,加大xms/xmx
  • 調優垃圾收集器,減少 full gc 中 stw 的時間,避免測活介面長時間停機。因為之前是jdk 8預設的(Parallel Scavenge + Parallel Old),換成了更適合高併發的(ParNew + CMS)。

2. jvm命令及引數

2.1. jstat -gcutil

注意:出於保密考慮,實際的資料不能在文中展示,下列展示的是與上下文無關的服務資料,是比較正常的 gc 過程資料:

[root@xxxapi data]# jstat -gcutil 1 1000
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
 50.07   0.00  53.80  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  57.11  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  62.04  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  64.26  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  66.61  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  69.18  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  71.68  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  74.75  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  77.20  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  80.12  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  83.09  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  87.77  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  89.82  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  92.29  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  94.57  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  99.00  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
  0.00  74.61   3.38  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61   7.88  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  11.46  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  14.93  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  18.23  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  21.29  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  25.22  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  28.74  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  31.64  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  36.85  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  39.30  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  44.76  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  48.55  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  51.25  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  54.17  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  58.48  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  61.99  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  64.52  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  67.25  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  70.92  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  74.60  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  78.43  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  82.41  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  86.26  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  90.79  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  93.74  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  95.90  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  99.28  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
 51.43   0.00   2.69  13.29  89.91  86.07   1084   21.345     5    2.055   23.400
 51.43   0.00   5.53  13.29  89.91  86.07   1084   21.345     5    2.055   23.400
 51.43   0.00   8.21  13.29  89.91  86.07   1084   21.345     5    2.055   23.400
  • S0:倖存1區當前使用比例
  • S1:倖存2區當前使用比例
  • E:伊甸園區使用比例
  • O:老年代使用比例
  • M:後設資料區使用比例
  • CCS:壓縮使用比例
  • YGC:年輕代垃圾回收次數
  • YGCT:年輕代垃圾回收消耗時間
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間

2.2. 物件進入老年代途徑

1. 物件年齡達到閾值後進入老年代

預設情況下,物件在新生代經歷了15次GC後,便會達到進入老年代的條件,將物件轉移進入老年代。當然,年齡的閾值可以透過JVM引數進行設定:

-XX:MaxTenuringThreshold=10
2. 大物件直接進入老年代

透過以下JVM引數進行設定:(注意此引數僅適用於Serial和ParNew兩款新生代收集器。)

-XX:PretenureSizeThreshold=5242880

原因:

  • 大物件需要連續的記憶體空間,而新生代為了安放大物件可能需要多次進行GC,增加開銷;
  • 新生代種伊甸園區和倖存者區常採用複製演演算法,需要經常複製物件到不同的區域,而大物件在複製時開銷較大。
3. 動態選擇進入老年代

HotSpot虛擬機器並不一定會嚴格按照設定的年齡閾值,滿足以下條件也能直接進入老年代:Survivor 區中,年齡從 1 到 n 的物件大小之和超過 Survivor 區的 50% 時,新生代中年齡大於等於 n 的物件將進入老年代。

注意一個誤區:這個物件大小總和是按年齡從小到大累加的,並不是同齡物件。

4. young gc 後溢位的進入老年代

在 young gc後,正常存活物件放入 survivor區,但如果放不下,存活物件溢位的部分,就會被放入老年代。

3. 垃圾收集器

3.1. 年輕代

3.1.1. Serial

Serial是一類用於新生代的單執行緒收集器,採用複製演演算法進行垃圾收集。Serial進行垃圾收集時,不僅只用一條單執行緒執行垃圾收集工作,它還在收集的同時,所用的使用者必須暫停。

  • 優勢:簡單高效,由於採用的是單執行緒的方法,因此與其他型別的收集器相比,對單個cpu來說沒有了上下文之間的的切換,效率比較高。
  • 缺點:會在使用者不知道的情況下停止所有工作執行緒,使用者體驗感極差,令人難以接受。
  • 適用場景:Client 模式(桌面應用);單核伺服器。
引數配置
  • -XX:+UserSerialGC: 選擇Serial作為新生代垃圾收集器

3.1.2. ParNew

ParNew收集器其實就是Serial的一個多執行緒版本,其在單核cpu上的表現並不會比Serail收集器更好,在多核機器上,其預設開啟的收集執行緒數與cpu數量相等。

當使用者執行緒都執行到安全點時,所有執行緒暫停執行,採用複製演演算法進行垃圾收集工作,完成之後,使用者執行緒繼續開始執行。

  • 優點:隨著cpu的有效利用,對於GC時系統資源的有效利用有好處。
  • 缺點:和Serial是一樣的。
  • 適用場景:ParNew是許多執行在Server模式下的虛擬機器中首選的新生代收集器。因為CMS收集器只能與serial或者parNew聯合使用,在當下多核系統環境下,首選的是ParNew與CMS配合。ParNew收集器也是使用CMS收集器後預設的新生代收集器。
引數配置
  • -XX:UseParNewGC: 新生代採用ParNew收集器
  • -XX:ParallelGCThreads: 設定JVM垃圾收集的執行緒數

3.1.3. Parallel Scavenge

Parallel Scavenge 也是一款用於新生代的多執行緒收集器,也是採用複製演演算法。與ParNew的不同之處在於:

Parallel Scavenge收集器的目的是達到一個可控制的吞吐量,而 ParNew 收集器關注點在於儘可能的縮短垃圾收集時使用者執行緒的停頓時間。

所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值, 即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)。
例如虛擬機器一共執行了 100 分鐘,其中垃圾收集花費了 1 分鐘,那吞吐量就是 99% 。比如下面兩個場景:

  1. 垃圾收集器每 100 秒收集一次,每次停頓 10 秒;
  2. 垃圾收集器每 50 秒收集一次,每次停頓時間 7 秒。

雖然後者每次停頓時間變短了,但是總體吞吐量變低了,CPU 總體利用率變低了

  • 優點:追求高吞吐量,高效利用CPU,是吞吐量優先,且能進行精確控制。
  • 適用場景:注重吞吐量高效利用CPU,需要高效運算,且不需要太多互動。
引數配置
  • -XX:+UseParallelOldGC: 預設使用 ParallelOldGC 時候預設新生代使用的是 ParallelScavenge 收集器
  • -XX:MaxGCPauseMilis: 控制最大垃圾收集停頓時間,引數值是一個大於0的毫秒數,收集器儘可能保證回收花費時間不超過設定值。但將這個值調小,並不一定會使系統垃圾回收速度更快,GC停頓時間是以犧牲吞吐量和新生代空間換來的。
  • -XX:GCTimeRadio: 設定吞吐量大小,引數值是一個(0,100)兩側均為開區間的整數。也是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。若把引數設定為19,則允許的最大GC時間就佔總時間的5%(1/(1+19))。預設值是99,即允許最大1%的垃圾收集時間。
  • -XX:+UserAdaptiveSizePolicy: 這是一個開關函式,當開啟這個函式,就不需要手動指定新生代的大小,Eden與Survivor區的比例(-XX:SurvivorRatio,預設是8:1:1),晉升老年代的物件年齡(-XX:PretenureSizeThreshold)等引數。JVM會動態調整這些引數,以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略.

3.2. 老年代

3.2.1. Serial Old

Serial Old是Serial收集器的老年代版本,同樣是一個單執行緒收集器,使用標記-整理演演算法

  • 適用場景:Client模式;單核伺服器;與Parallel Scavenge收集器搭配;作為CMS收集器的後備方案,在併發收集發生Concurrent Mode Failure時使用

3.2.2. Parallel Old

Parallel Old是 Parallel Scavenge 收集器的老年代版本,使用 多執行緒和標記-整理演演算法,可以充分利用多核CPU的計算能力。

  • 適用場景:注重吞吐量與CPU資源敏感的場合,與Parallel Scavenge 收集器搭配使用,jdk7 和 jdk8 預設使用該收集器作為老年代收集器
引數配置
  • -XX:+UserParallelOldGC: 開啟 ParallelScavenge + ParallelOld

3.2.3. CMS

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。採用的演演算法是“標記-清除”,運作過程分為四個步驟

  • 初始標記,標記GC Roots 能夠直接關聯到達物件
  • 併發標記,進行GC Roots Tracing 的過程
  • 重新標記,修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄
  • 併發清除,用標記清除演演算法清除物件。

其中“初始標記”和“重新標記”這兩個步驟仍然需要STW(stop the world)。耗時最長的“併發標記”與“併發清除”過程收集器執行緒都可以與使用者執行緒一起工作。總體上來說CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

  • 優點:併發收集,低停頓
  • 缺點:
  • CMS收集器對CPU資源非常敏感,CMS預設啟動對回收執行緒數(CPU數量+3)/4,當CPU數量在4個以上時,併發回收時垃圾收集執行緒不少於25%,並隨著CPU數量的增加而下降,但當CPU數量不足4個時,對使用者影響較大。
  • CMS無法處理浮動垃圾,可能會出現“Concurrent Mode Failure”失敗而導致一次FullGC的產生。這時會地洞後備預案,臨時用SerialOld來重新進行老年代的垃圾收集。由於CMS併發清理階段使用者執行緒還在執行,伴隨程式執行自然還會有新的垃圾產生,這部分垃圾出現在標記過程之後,CMS無法在當次處理掉,只能等到下一次GC,這部分垃圾就是浮動垃圾。同時也由於在垃圾收集階段使用者執行緒還需要執行,那也就需要預留足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他老年代幾乎完全填滿再進行收集。可以透過引數-XX:CMSInitiatingOccupancyFraction修改CMS觸發的百分比。
  • 因為CMS採用的是標記清除演演算法,因此垃圾回收後會產生空間碎片。透過引數可以進行最佳化。
引數配置
  • -XX:+UseConcMarkSweepGC: 啟用cms
  • -XX:ConcGCThreads: 併發的GC執行緒數
  • -XX:+UseCMSCompactAtFullCollection: FullGC之後做壓縮整理(減少碎片)
  • -XX:CMSFullGCsBeforeCompaction: 多少次FullGC之後壓縮一次,預設是0,代表每次FullGC後都會壓縮一次
  • -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(預設是92,這是百分比)
  • -XX:+UseCMSInitiatingOccupancyOnly: 只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),如果不指定,JVM僅在第一次使用設定值,後續則會自動調整
  • -XX:+CMSScavengeBeforeRemark: 在CMS GC前啟動一次minor gc,降低CMS GC標記階段(也會對年輕代一起做標記,如果在minor gc就幹掉了很多對垃圾物件,標記階段就會減少一些標記時間)時的開銷,一般CMS的GC耗時 80%都在標記階段
  • -XX:+CMSParallellnitialMarkEnabled: 表示在初始標記的時候多執行緒執行,縮短STW
  • -XX:+CMSParallelRemarkEnabled: 在重新標記的時候多執行緒執行,縮短STW;

3.4. 總結

可以搭配使用的垃圾收集器包括:

  • Serial + Serial old
  • Serial + CMS
  • ParNew + Serial old
  • ParNew + CMS
  • Parallel Scavenge + Serial 0ld
  • Parallel Scavenge + Parallel 0ld

目前常用的搭配如下:

  • Parrallel Scavenge + Parrallel OldJDK 8 預設收集器搭配。吞吐量優先,後臺任務型服務適合;
  • ParNew + CMS:經典的低停頓蒐集器,絕大多數商用、延時敏感的服務在使用;
  • G1JDK 9 預設收集器,堆記憶體比較大(6G-8G以上)的時候表現出比較高吞吐量和短暫的停頓時間;

相關文章