线上FullGC问题如何排查 - Java版

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问题的排查在后面遇到类似的问题,处理起来也会清晰很多。

相关推荐
洗澡水加冰1 分钟前
Trae说: Nuxt or Next选一个
后端·trae
不懂英语的程序猿3 分钟前
【SF顺丰】顺丰开放平台API对接(注册、API测试篇)
前端·后端
林夕11204 分钟前
一文搞懂 Express:使用、路由、中间件与源码
javascript·后端·node.js
异常君9 分钟前
分布式锁隐患解析:当业务执行时间超过锁过期时间的完整对策
java·redis·后端
season_zhu11 分钟前
iOS开发:关于Moya之上的Request层
ios·架构·swift
扎瓦15 分钟前
Java 动态代理
java·后端·面试
EMQX17 分钟前
MQTTX + MCP:MQTT 客户端秒变物联网 Agent
后端
何双新20 分钟前
企业AI应用模式解析:从本地部署到混合架构
人工智能·架构
Blossom.11841 分钟前
量子计算在金融领域的应用与展望
数据库·人工智能·分布式·金融·架构·量子计算·ai集成