FullGC 常见问题思考
Q1:现网系统发生频繁 FullGC (约每10分钟一次),登陆机器发现 JVM 参数只配置了最大堆内存,其他配置都是系统默认配置,请问如何排查并优化 JVM 内存。
Q2:系统出现 java.lang.OutOfMemoryError: Java heap space,如何排查和快速恢复?
Q3:系统出现 java.lang.OutOfMemoryError: MetaSpace,如何排查和快速恢复?
1.JVM 堆内存分配
问题1:新生代跟老年代默认的比例分配
问题2:新生代中的 Eden 区、FromSuv 区,ToSuv 区默认的分配比例
JVM 内存参数说明:
① -Xms 设置堆内存初始值,堆内存 = 新生代 + 老年代,默认是物理内存的 1/64
② -Xmx 设置堆内存最大值,默认是物理内存的 1/4
③ -Xmn 设置年轻代的大小
④ -XX:SurvivorRatio=8 设置新生代中 Eden 和 Survivor 的比值。8 表示Survivor : Eden = 1 : 8,两个 FromSuv 和 ToSuv 占比相等,即 Eden : FromSuv : FromSuv = 8 : 1 : 1
⑤ -XX:PretenureSizeThreshold=1024 设置对象进入老年区的阈值,即该对象大小超过 1024 KB 直接进入老年区
⑥ -XX:MetaspaceSize 设置元空间初始值
⑦ -XX:MaxMetaspaceSize 元空间最大值
JVM 堆内存模型
默认的 Young : Old = 1 : 2,例如 -Xmx2048m,-Xmn2014m。默认的 Eden : from : to = 8 : 1 : 1,即 -XX:SurvivorRatio=8
image.png2.如何确定频繁 FullGC
FullGC 频繁发生的特征:
① 系统的进程 CPU 接近 100%,经过 jstack 命令排查,发现多个垃圾回收线程的 CPU 占用很高;
② 采用 jstat 命令查看 GC 情况,FullGC 的发生次数很多,同时次数不断增加;
(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 条
// 结果可知,FullGC 次数高达 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
现网如果频繁发生的 FullGC,使得系统运行缓慢,导致系统不可用,那么首先采用 jstack 和 jmap 导出堆栈和内存信息,然后重启系统。
(1)老年代发生 FullGC
方案1: 内存调优
根据 FullGC 后的老年代大小,调整堆内存、新生代、老年代和永久代。
① JVM 堆,建议 3~4 倍 FullGC 后的老年代大小
② 新生代,建议 1~1.5 倍 FullGC 后的老年代大小
③ 老年代,建议 2~3 倍 FullGC 后的老年代大小
④ 永久代即元空间,建议 1.2~1.5 倍 FullGC 后的老年代大小
方案2:排查大对象和内存溢出
如果上述的内存调优后,依然出现频繁 FullGC,需要采用内存分析工具,例如 MAT,分析 heapdump 日志。如下图所示,System、AppClassLoader 对象比较大,需要进一步分析业务代码中哪里创建或者频繁调用相关类。
image.png(2)Metaspace 发生 FullGC
Metaspace 的背景知识
① Metaspace 的作用:存储 JVM 加载的类信息、常量、静态变量等
② 设置 Metaspace 的参数:-XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N
③ 类加卸载的参数:-XX:+TraceClassLoading -XX:+TraceClassUnloading
④ Metaspace 中的类被垃圾回收的条件:a.该类的所有实例被垃圾回收;b.该类对应的 java.lang.Class 对象没有被任何地方引用;c.加载该类的 ClassLoader 已经被垃圾回收;
问题现象:在 flink session on k8s 模式下,每次启动 flink jar 作业都会使得 Metaspace 增长,最终导致 java.lang.OutOfMemoryError: Metaspace。
解法步骤:
① 添加监控,例如 flink 容器内存、jobmanager heap 和 no-heap 内存、ClassLoader 加载类数量等监控信息。根据监控初步定位到,每次启动作业都会加载类的数量为 1.5k。
② 了解 flink 的类加载机制后,修改 flink-conf.yaml 配置的 classloader.resolve-order: parent-first。结果是作业每次启动还是增加 1.5k 的类,且 FullGC 无法回收 Metaspace 的内存,通过 MAT 分析可知 ClassLoader 较大且数量多。
③ 检查 flink jar 作业的业务流程,发现该 jar 是瘦包,即作业在启动过程中会加载自身 lib 目录下的 jar,导致 flink 的配置 classloader.resolve-order: parent-first 不起作用。因此,把该作业的打包方式修改为肥包,其结果是作业每次启动减少到了 0.9k 的类,但仍然会导致 Metaspace OOM,同时通过 MAT 分析可知 ClassLoader 较大且数量多。
④ 添加 JVM 参数:-XX:TraceClassLoading -XX:TraceClassUnloading,查看加载类的具体信息。从打印出的类信息可知,作业在加载自研类的同时也会加载该类的依赖,即类加载的全盘负责机制,使其加载了 0.9k 的类。
参考
[1] 谨防 JDK8 重复类定义造成的内存泄漏
[2] 从一起 GC 血案谈到反射原理
网友评论