FullGC 常見問題思考
Q1:現(xiàn)網(wǎng)系統(tǒng)發(fā)生頻繁 FullGC (約每10分鐘一次),登陸機器發(fā)現(xiàn) JVM 參數(shù)只配置了最大堆內(nèi)存,其他配置都是系統(tǒng)默認配置,請問如何排查并優(yōu)化 JVM 內(nèi)存。
Q2:系統(tǒng)出現(xiàn) java.lang.OutOfMemoryError: Java heap space,如何排查和快速恢復?
Q3:系統(tǒng)出現(xiàn) java.lang.OutOfMemoryError: MetaSpace,如何排查和快速恢復?
1.JVM 堆內(nèi)存分配
問題1:新生代跟老年代默認的比例分配
問題2:新生代中的 Eden 區(qū)、FromSuv 區(qū),ToSuv 區(qū)默認的分配比例
JVM 內(nèi)存參數(shù)說明:
① -Xms 設置堆內(nèi)存初始值,堆內(nèi)存 = 新生代 + 老年代,默認是物理內(nèi)存的 1/64
② -Xmx 設置堆內(nèi)存最大值,默認是物理內(nèi)存的 1/4
③ -Xmn 設置年輕代的大小
④ -XX:SurvivorRatio=8 設置新生代中 Eden 和 Survivor 的比值。8 表示Survivor : Eden = 1 : 8,兩個 FromSuv 和 ToSuv 占比相等,即 Eden : FromSuv : FromSuv = 8 : 1 : 1
⑤ -XX:PretenureSizeThreshold=1024 設置對象進入老年區(qū)的閾值,即該對象大小超過 1024 KB 直接進入老年區(qū)
⑥ -XX:MetaspaceSize 設置元空間初始值
⑦ -XX:MaxMetaspaceSize 元空間最大值
JVM 堆內(nèi)存模型
默認的 Young : Old = 1 : 2,例如 -Xmx2048m,-Xmn2014m。默認的 Eden : from : to = 8 : 1 : 1,即 -XX:SurvivorRatio=8

2.如何確定頻繁 FullGC
FullGC 頻繁發(fā)生的特征:
① 系統(tǒng)的進程 CPU 接近 100%,經(jīng)過 jstack 命令排查,發(fā)現(xiàn)多個垃圾回收線程的 CPU 占用很高;
② 采用 jstat 命令查看 GC 情況,F(xiàn)ullGC 的發(fā)生次數(shù)很多,同時次數(shù)不斷增加;
(1)垃圾回收線程的 CPU 占用高
// 使用 top 查看 CPU 占用情況
// 結果可知, PID 為 9 CPU 占用是 98%
[root@TG1704 log]# top
top - 08:31:10 up 30 min, 0 users, load average: 0.73, 0.58, 0.34
KiB Mem: 2046460 total, 1923864 used, 122596 free, 14388 buffers
KiB Swap: 1048572 total, 0 used, 1048572 free. 1192352 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9 root 20 0 2557160 288976 15812 S 98.0 14.1 0:42.60 java
// 使用 top -H -p 9 表示查看進程號 為 9 中的線程號
// 結果可知,線程號為 10 的 CPU 占用是 79.3%,而 11 的 是 13.2%
[root@TG1704 log]# top -H -p 9
top - 08:31:16 up 30 min, 0 users, load average: 0.75, 0.59, 0.35
Threads: 11 total, 1 running, 10 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.5 us, 0.6 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 2046460 total, 1924856 used, 121604 free, 14396 buffers
KiB Swap: 1048572 total, 0 used, 1048572 free. 1192532 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10 root 20 0 2557160 289824 15872 R 79.3 14.2 0:41.49 java
11 root 20 0 2557160 289824 15872 S 13.2 14.2 0:06.78 java
// jstack -l 9 > jstack_9.log 表示進程號為 9 的堆棧信息保存到 jstack_9.log 文件
// 結果可知,10 轉換為十六進制 0xa,最后一行的 nid=0xa, VM Thread 指的就是垃圾回收的線程
[root@TG1704 bin]# jstack -l 10 > jstack_10.log
[root@TG1704 bin]# cat jstack_10.log
"main" #1 prio=5 os_prio=0 tid=0x00007f8718009800 nid=0xb runnable [0x00007f871fe41000]
java.lang.Thread.State: RUNNABLE
at com.aibaobei.chapter2.eg2.UserDemo.main(UserDemo.java:9)
"VM Thread" os_prio=0 tid=0x00007f871806e000 nid=0xa runnable
(2)查看 GC 情況
// jstat -gcutil 9 1000 5 表示查詢進程號為 9,每 1000 毫秒顯示 5 條
// 結果可知,F(xiàn)ullGC 次數(shù)高達 6793,同時不斷增長
[root@TG1704 bin]# jstat -gcutil 9 1000 5
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 0.00 75.07 59.09 59.60 3259 0.919 6517 7.715 8.635
0.00 0.00 0.00 0.08 59.09 59.60 3306 0.930 6611 7.822 8.752
0.00 0.00 0.00 0.08 59.09 59.60 3351 0.943 6701 7.924 8.867
0.00 0.00 0.00 0.08 59.09 59.60 3397 0.955 6793 8.029 8.984
3.如何排查頻繁 FullGC
現(xiàn)網(wǎng)如果頻繁發(fā)生的 FullGC,使得系統(tǒng)運行緩慢,導致系統(tǒng)不可用,那么首先采用 jstack 和 jmap 導出堆棧和內(nèi)存信息,然后重啟系統(tǒng)。
(1)老年代發(fā)生 FullGC
方案1: 內(nèi)存調(diào)優(yōu)
根據(jù) FullGC 后的老年代大小,調(diào)整堆內(nèi)存、新生代、老年代和永久代。
① JVM 堆,建議 3~4 倍 FullGC 后的老年代大小
② 新生代,建議 1~1.5 倍 FullGC 后的老年代大小
③ 老年代,建議 2~3 倍 FullGC 后的老年代大小
④ 永久代即元空間,建議 1.2~1.5 倍 FullGC 后的老年代大小
方案2:排查大對象和內(nèi)存溢出
如果上述的內(nèi)存調(diào)優(yōu)后,依然出現(xiàn)頻繁 FullGC,需要采用內(nèi)存分析工具,例如 MAT,分析 heapdump 日志。如下圖所示,System、AppClassLoader 對象比較大,需要進一步分析業(yè)務代碼中哪里創(chuàng)建或者頻繁調(diào)用相關類。

(2)Metaspace 發(fā)生 FullGC
Metaspace 的背景知識
① Metaspace 的作用:存儲 JVM 加載的類信息、常量、靜態(tài)變量等
② 設置 Metaspace 的參數(shù):-XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N
③ 類加卸載的參數(shù):-XX:+TraceClassLoading -XX:+TraceClassUnloading
④ Metaspace 中的類被垃圾回收的條件:a.該類的所有實例被垃圾回收;b.該類對應的 java.lang.Class 對象沒有被任何地方引用;c.加載該類的 ClassLoader 已經(jīng)被垃圾回收;
問題現(xiàn)象:在 flink session on k8s 模式下,每次啟動 flink jar 作業(yè)都會使得 Metaspace 增長,最終導致 java.lang.OutOfMemoryError: Metaspace。
解法步驟:
① 添加監(jiān)控,例如 flink 容器內(nèi)存、jobmanager heap 和 no-heap 內(nèi)存、ClassLoader 加載類數(shù)量等監(jiān)控信息。根據(jù)監(jiān)控初步定位到,每次啟動作業(yè)都會加載類的數(shù)量為 1.5k。
② 了解 flink 的類加載機制后,修改 flink-conf.yaml 配置的 classloader.resolve-order: parent-first。結果是作業(yè)每次啟動還是增加 1.5k 的類,且 FullGC 無法回收 Metaspace 的內(nèi)存,通過 MAT 分析可知 ClassLoader 較大且數(shù)量多。
③ 檢查 flink jar 作業(yè)的業(yè)務流程,發(fā)現(xiàn)該 jar 是瘦包,即作業(yè)在啟動過程中會加載自身 lib 目錄下的 jar,導致 flink 的配置 classloader.resolve-order: parent-first 不起作用。因此,把該作業(yè)的打包方式修改為肥包,其結果是作業(yè)每次啟動減少到了 0.9k 的類,但仍然會導致 Metaspace OOM,同時通過 MAT 分析可知 ClassLoader 較大且數(shù)量多。
④ 添加 JVM 參數(shù):-XX:TraceClassLoading -XX:TraceClassUnloading,查看加載類的具體信息。從打印出的類信息可知,作業(yè)在加載自研類的同時也會加載該類的依賴,即類加載的全盤負責機制,使其加載了 0.9k 的類。