一次性能测试中JVM频繁Full GC的解决经验总结

问题现象

         最近对平台功能进行性能优化。功能完成后,开始在环境进行长稳测试。使用模拟客户端以每秒15000TPS向系统发送请求,结果发现系统性能总是保持一段时间后开始下降,最坏的情况只有8000到9000TPS左右,之后开始上升下降反复来回。

                                    一次性能测试中JVM频繁Full GC的解决经验总结_第1张图片

 

问题分析

         系统由java编写,性能出现锯齿形波动的现象,猜测可能是由于java虚拟机(JVM)出现Full GC(垃圾回收)现象导致。我们知道,当JVM进行Full GC时,进程会出现暂停响应(stop the world)的现象。为了确认这一点,我在java启动命令行参数中加上输出gc日志的参数。

         XX:+PrintGCDetails -Xloggc:dcc_gc.log

         通过gc日志,我们可以明显看到,当测试进行一段时间后,JVM发生了Full GC,而且次数比较频繁,直接影响性能测试。可以看到下图中有Full GC的日志。

一次性能测试中JVM频繁Full GC的解决经验总结_第2张图片

   

       JVM触发Full GC的条件是什么呢?下面先介绍下java一些知识。

       JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)。

                               一次性能测试中JVM频繁Full GC的解决经验总结_第3张图片

       上图是java运行时数据存储模型,包括栈,堆,方法区等等。程序内存数据一般都在堆中进行管理。Java堆又分为新生代和老年代,新生代可以分为Eden、Survivor。新建的对象会存放到新生代中,当多次垃圾回收后,仍然存活的对象会转移到老年代中。因此对象存活路径为:Eden->Survivor->Old Generation。

                              一次性能测试中JVM频繁Full GC的解决经验总结_第4张图片

       当老年代空间不足时,这时就会发生Full GC。明白了JVM内存管理机制,下面就开始进行优化。

第一次优化

       那么上面性能测试现象是不是因为内存不够导致的呢。我又对命令行参数添加内存设置参数:

      -Xmx30g -Xms30g -Xmn4g

      Xmx:进程最大内存,这里设置为30G。

      Xms:进程初始内存,和Xmx设置一样。

      Xmn:新生代大小,设置为4g。

      通过以上设置,老年代的内存大小为30-4=26G,已经足够大了。继续测试发现,Full GC频率有所下降,而且GC时间比较短。但是Full GC现象还是会间隔发生,并会影响响应时长。(单位为us秒)。

一次性能测试中JVM频繁Full GC的解决经验总结_第5张图片

                              一次性能测试中JVM频繁Full GC的解决经验总结_第6张图片

 

         看来通过进程内存参数设置,不能从根本上解决问题。

第二次优化

         既然参数调整没法解决问题,那么只能深层次分析内存中到底是哪些对象占用了如此大的内存。这时,需要使用到几个java内存分析工具:jmap,MAT等。

         首先使用jmap工具,导出整个JVM 中内存信息。命令如下:

         jmap -dump:format=b,file=文件名 [pid]

         由于进程内存比较大,dump过程比较缓慢,耐心等待就好。大概等待20分钟后,终止导出来30G大小的dump文件。由于文件太大,一般jvisualvm分析工具难以加载。经过查阅资料,我是用MAT工具对dump文件进行分析。

        ./ParseHeapDump.sh m.hprof  org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components

        m.hprof就是jvm的dump文件,在mat目录下会生成3份.zip结尾的报告和一些m.相关的文件,将生成的m.hprof相关的文件都下载到windows本地磁盘。由于dump文件比较大,以上过程进行比较缓慢,因此可以放到linux后台运行。以下是生成的报告内容。

                   一次性能测试中JVM频繁Full GC的解决经验总结_第7张图片

        在报告中,我发现RingBuff这个实例特别奇怪,只有一个实例,但是占用的空间非常大。为此,这时我开始查阅程序源代码。

        通过走读源代码,了解到系统是基于Netty+Disruptor框架编写。Disruptor是一个开源框架,并发性能非常强悍。它使用了一个叫RingBuffer环形队列的数据结构,避免使用同步锁,因此性能非常高。

                                       一次性能测试中JVM频繁Full GC的解决经验总结_第8张图片

        RingBuffer环形队列里面的数据不会释放,当队列满时会覆盖前面的数据。这样一来,随着系统的运行最终肯定有buffersize大小的数据长驻内存中,不会被垃圾回收器回收。

         经过以上分析,我立即查看环境中RingBufferSize配置,果不其然,这个参数被设置的很大:6291456。意味着整个环形队列有6291456个槽位,而RingBuffer里面存放的是Cdr话单对象,这个对象里面有个HashMap,可能包含几十到上百个话单字段。如果一个话单对象有几百个字节,那么整个队列将占有几G甚至几十G的内存。怪不得老年代内存不够用,会频繁发生GC。

        综合考虑性能测试目标,我将RingBufferSize设置为2048,经过近1亿数据量的测试,终于再没有出现Full GC的情况。

 

你可能感兴趣的:(一次性能测试中JVM频繁Full GC的解决经验总结)