Java线上FullGC问题如何排查
在线上环境,我们可能会经常遇到程序执行缓慢,甚至频繁出现卡顿的情况。有时候通过top命令查看进程,甚至出现cpu运行接近100%
的情况。如果线上我们接入了监控告警,可能此时就会收到大量的GC告警。这些现象的产生,很可能是我们的程序在频繁的发生FullGC
频繁的发生FullGC是程序一个隐藏的非常严重的问题,严重影响到服务的性能,对于FullGC问题的定位进行一个思路分析

GC优化指标
满足以下两个条件一般认为GC就是正常的,否则就要进行优化了
- Minor GC执行时间不到50ms,Minor GC执行不频繁,约10秒一次;
- Full GC执行时间不到1s,Full GC执行频率不算频繁,约10分钟一次;
案例分析
假设我们有以下SpringBoot的web工程,其controller代码如下:
java
@RestController
public class GcController {
static class StaticGcObject {
}
@GetMapping("/gcDemo")
public String ooM() throws InterruptedException {
List<StaticGcObject> list = new ArrayList<>();
int i = 0;
Thread.sleep(10000);
while(true) {
list.add(new StaticGcObject());
i++;
System.out.println(i);
System.out.println(list.size());
}
}
}
这个controller的逻辑是有一个list对象,然后触发接口后,会不断的往list集合中添加StaticGcObject对象,导致list越来越大
1.场景模拟
在IDE里面配置上jvm和gc相关的参数

参数命令如下:
ini
-Xmx60m
-Xms60m
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/Users/lisirui/Documents/gcDemo/gc-demo.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/lisirui/Documents/gcDemo/fullGC-heap-dump.hprof
设置运行内存大小为60M,根据默认的分配比例(1:2)可以得到,年轻代 : 老年代 = 20M : 40M,年轻代中根据 XX:SurvivorRatio=8
可以得到eden : s0 : s1 = 16M : 2M : 2M。然后通过-Xlog:gc选项将GC日志生成到/Users/lisirui/Documents/gcDemo/gc-demo.log文件
2.启动项目
在浏览器访问http://localhost:8100/gcDemo,可以看到一直处于等待状态,过一会,会出现错误页面

在程序控制台可以看到堆内存溢出的报错,是正常情况,不影响我们分析GC的问题

3.收集GC日志
到刚刚在VM Option设置jvm参数指定的gc日志路径下,可以看到生成了的GC日志文件gc-demo.log

4.分析GC日志
打开gc-demo.log文件,确实在发生频繁的FullGC,可以初步判断,程序反应慢以及最后的报错跟GC有关

分析gc-demo.log日志文件的时候可以直接打开手动分析,查找GC关键字,初略分析GC的频率和耗时。
也可以用GCViewer工具打开GC日志文件来分析,后者对于一些指标的观察可能更直观,比如GC过程中的总耗时,平均暂停间隔时间等
5.监控JVM GC情况
打开VisualVM工具,打开控制台输入jvisualvm即可

在打开的左侧可以看到运行的java进程,找到我们当前测试的进程,然后右边的Visual GC一开始可能是没有的,需要到插件市场下载



可以看到图中,Eden进行了47此GC,虽然时间短,但是能说明一个问题,新生代堆内存分配的空间小,导致Eden区频繁GC
从Survivor区可以看出,基本没有GC,说明这些都是大对象,直接进入到了Old老年代

到后面会发现,Old老年代在进行频繁的GC,次数达到140+,因为Eden区的对象要进入Old老年代,但是Old放不下了,就频繁GC
所以我们需要加大新生代和老年代的内存大小,同时减少大对象的产生
6.JVM参数调整
从上面的分析来看,GC发生很频繁,所以我们适当的调大JVM的内存大小,同时开启Heap Dump文件,JVM参数修改如下:
ini
-Xmx512m
-Xms512m
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/Users/lisirui/Documents/gcDemo/gc-demo.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/lisirui/Documents/gcDemo/fullGC-heap-dump.hprof
然后启动程序,在浏览器访问http://lovalhost:8100/geDemo,可以看到前端页面一直处于等待状态

后台程序一直在运行中,过了一段时间报错,查看后台程序的运行情况,报了堆内存溢出的Error

但是这次的等待时间相比上一次明显要更久,再次打开VisualVm工具,观察GC情况

在相同的时间内,60M和512M的内存大小分配明显的GC次数会少很多,但是到后面仍然发现Old老年代始终会满

7.分析Heap Dump文件
通过加大JVM的内存,一定程度上优化了GC频繁的问题,但是Old老年代仍然存在问题,在运行一段时间后,程序依旧报堆内存溢出Error
这说明我们的程序代码是又问题的,所以进一步分析Heap Dump文件。
回顾我们刚刚指定的JVM参数,发现在目录下生成了fullGC-heap-dump.hprof文件
ini
-XX:HeapDumpPath=/Users/lisirui/Documents/gcDemo/fullGC-heap-dump.hprof

用VisualVM工具打开fullGC-heap-dump.hprof文件

发现在Controller类中的StaticGcObject类型占用了大量的内存,而且这个类型的对象数量非常多,问题就出在这。
8.代码审查
通过上一步的heap dump文件分析,我们确定了在代码中存在占据大量内存的对象类型StaticGcObject,去代码中定位

代码发现存在一个StaticGcObject类型的列表对象list,并且在这个controller中,通过访问触发之后,会在一个死循环中不停的创建新的
StaticGcObject对象越来越多,list越来越大,由于没有做限制,最终肯定会导致堆内存溢出
9.代码优化
简单优化,对于无上限的list添加做一个限制,当达到指定大小之后,就不允许添加,这个根据实际场景找出代码问题去优化即可
java
@RestController
public class GcController {
private static final int MAX_SIZE = 100;
static class StaticGcObject {
}
@GetMapping("/gcDemo")
public String ooM() throws InterruptedException {
List<StaticGcObject> list = new ArrayList<>();
int i = 0;
Thread.sleep(10000);
while(true) {
if(list.size() > MAX_SIZE) {
return "OOM Test Over";
}
list.add(new StaticGcObject());
i++;
System.out.println(i);
System.out.println(list.size());
}
}
}
10.方案验证
优化完代码再次启动程序,访问http://localhost:8100/gcDemo,返回结果如下

再次打开VisualVM工具观察,查看一下GC情况

小结
首先是要识别出什么场景下可能发生线上的频繁FullGC,一般程序发生频繁FullGC都会造成程序运行缓慢,出现卡顿的情况。
一旦出现这种情况,就要考虑FullGC的问题了。识别出现象之后,就要开始收集堆内存日志,分析日志,以及利用JVM内存分析工具
查看GC情况,如果确实是发生了频繁的FullGC,通过JVM内存分析工具比如VisualVM都能直观的看到GC发生次数,频率,以及耗时
然后针对性的进行处理,比如调整JVM参数,最后如果效果还是不理想,一般还需要继续分析heap dump文件来定位代码问题
通过梳理排查FullGC问题的思路,对FullGC问题的排查在后面遇到类似的问题,处理起来也会清晰很多。