最近上线了一个需求,遇到了一个JVM报警的问题,很荣幸能遇到,在此分享一下整个调优的过程。
我们是中台服务,我们的甲方就是上游不同的业务。中台原则上是业务和能力分离,但是不可避免的是分不开,所以我们通过SPI的机制让上游的业务实现SPI接口从而执行他们自己的逻辑。本次需求我们上线了一个大需求,要同时发布很多业务方实现的SPI包。我们是灰度发布,发布一台机器后发现频繁的FGC导致监控报警。
补充一下我们的机器规格是4核4G内存和80G磁盘。GC垃圾收集器是CMS和ParNew。
接下来开始进行推理和论证。
频繁的FGC初步想法就是OOM,比如静态集合无限添加对象。但是去机器上找了一下OOM的Dump文件这个是没找到的,所以说只能手动dump。
jmap -dump:format=b,file=/tmp/myapp_dump.bin pid1234
使用MAT去分析,去找自己的类最多的,发现并没有找到那种一枝独秀特别多的。下面是反例,公司不方便截图。
因为是灰度,所以我们有正常的机器。对比了有问题机器和没问题机器新生代和老年带的变化趋势、速成和使用大小,整体是相似的,这块就不符合常理了。类似下面的图。
结论:新生代老年代和正常机器一样,初步推断堆空间正常。
如果不是堆空间引起的FGC,那就是元空间要满了。接着通过arthas的dashboard命令对比,这里发现这个值新老机器差距很大。
那就需要调大元空间
把
-XX:MetaspaceSize=1500m
-XX:MaxMetaspaceSize=1500mm
修改为
-XX:MetaspaceSize=2048m
-XX:MaxMetaspaceSize=2048m
此时机器再发布后就不会出现FGC了。
结论:元空间小,导致频繁FGC
通过JVM的命令,可以看出来加载了哪些类
jcmd 28818 GC.class_histogram
那我看上图有什么意义呢? 对比新老机器,可以获得两份加载的类 通过awk命令能洗出来有哪些类,然后通过diff命令就可以看出来不同的类。
通过分析两个加载的类不同,发现两个问题。
补充一下有意义的jvm启动参数
-XX:ParallelGCThreads=4 (并行收集,几核机器设置几核)
-Xms6g (调优,设置新生代初始大小)
-Xmx6g (调优,设置新生代最大值)
-Xmn2g (调优,设置堆空间大小)
-XX:MetaspaceSize=2048m
-XX:MaxMetaspaceSize=2048m
-XX:MaxDirectMemorySize=1g
-XX:SurvivorRatio=8 (新老年代默认8:1:1)
-XX:+UseConcMarkSweepGC (使用CMS垃圾收集器)
-XX:CMSMaxAbortablePrecleanTime=5000 (并发标记阶段之后、重新标记阶段之前,就让你执行这么长时间)
-XX:+CMSClassUnloadingEnabled (允许类卸载,比如线上使用内存诊断工具Arthas,用完后会有残留)
-XX:CMSInitiatingOccupancyFraction=80 (老年带到达80%,触发老年代收集)
-XX:+UseCMSInitiatingOccupancyOnly(配合上面参数使用)
-XX:+ExplicitGCInvokesConcurrent (针对System.gc()触发老年带的GC,否则就是fullGC)
-Xloggc:/home/admin/logs/gc.log (GC日志目录)
-XX:+PrintGCDetails (GC日志详细细节)
-XX:+PrintGCDateStamps(每个垃圾收集事件发生的确切日期和时间戳)
-XX:+HeapDumpOnOutOfMemoryError (OOM)
-XX:HeapDumpPath=/home/admin/logs/java.hprof (OOM)