《Java 虛擬機原理》7.4 精選 —— FullGC 篇

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

image.png

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)用相關類。

image.png

(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 的類。

參考

[1] 謹防 JDK8 重復類定義造成的內(nèi)存泄漏
[2] 從一起 GC 血案談到反射原理

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容