JVM堆内存导致的FGC问题排查
问题发现
上次我们说了堆外内存导致的FGC:JVM堆外内存导致的FGC问题排查
这次线上环境又在频繁的FGC,问题是在堆内,jvm调优箭在弦上。
堆空间分区
堆空间分区说明
Eden: 伊甸园区,新创建对象存储区域
Survivor Memory spaces (S0, S1): 幸存区,发生minor gc时,幸存区的对象全部复制到另一个里面去。
Old:老年代,存放经过几次垃圾回收幸存下来生命周期较长的对象。
对象在堆中的生命周期
新生代对象分配到Eden,当 Eden 空间被对象填充时,会执行 Minor GC(也称为 Young Collection),并将所有s0内的对象移动到s1。Minor GC 还会检查幸存者对象并将它们移动到其他幸存者空间。所以一次,一个幸存者空间总是空的。经过多次 GC 循环后幸存的对象被移动到老年代内存空间。通常是通过在年轻代对象有资格升级到老年代之前设置一个阈值来完成的。
快照下载
使用命令 dump堆内存
jmap -heap:file=/path pid
得到堆文件后,便可以使用工具来分析了。
快照分析工具
Eclipse MAT
下载地址:https://www.eclipse.org/mat/
界面如下,可以看到对象占用比例。数据直方图,使用的最舒服的是,有内存泄露自动分析
内存泄露分析:
可以看到这个工具给出了内存泄露的怀疑点。
VirtualVM
下载地址:https://visualvm.github.io/
界面如下,virtualVM主要是用来看对象的数量以及大小。
JProfile
下载地址:https://www.ej-technologies.com/products/jprofiler/overview.html
这个是收费的,我的已经过期了,所以没法截图了。大家自行研究吧。
YourKit
下载地址:https://www.yourkit.com/
该工具功能较多,用起来很爽。推荐!
首先常规的对象列表肯定是有的:
然后还可以逐一分析问题,例如重复字符串,重复对象等。
还可以查看垃圾对象的内容
还有其他的一些功能,可自行体验。
jxray
下载地址:https://jxray.com/
jxray 是我的一大杀器。他可以根据堆dump生成一份报告出来,非常的详细。但是这个没有可视化页面,需要命令。下面我演示下:
首先下载,下载完是一个jar包,还有一个shell。按照下图的命令去执行:
然后输入你的邮箱,它会发一个key到你的邮箱,
输入key之后会开始执行分析了。
执行完毕后会生成报告,是一个网页。我们打开
看到没,绿色的代表没问题,黄色是需要注意的,红色的是可能得问题。点开之后还会有详细的问题分析以及解决方案。非常的给力。
GC easy
下载地址:https://gceasy.io/
这个是在线的gc日志分析网站。不同于堆内存分析,它是分析的gc日志。需要开启参数
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
问题猜想
回到问题中来。在经过上面这几种工具分析后,其实我这边并没有发现内存泄露或者其他问题。并且对代码进行了详细的review。
我使用的jvm参数是:
-server
-Xmx6g
-Xms6g
-XX:NewRatio=1
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:MaxTenuringThreshold=15
-XX:SurvivorRatio=3
-XX:+ParallelRefProcEnabled
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:+HeapDumpOnOutOfMemoryError
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/export/Logs/gc.log
-XX:+PrintTenuringDistribution
根据分析的情况。服务的jvm表现不好,我需要优化的点在于:在发生Full GC之前,minor gc并没有很频繁。也就是minor gc次数较少,major gc较频繁。并且major gc回收率较高(如下图),也就是说有部分老年代对象并不是long live objects
第一次尝试 - 新生代大小
既然是minor gc少,经过不几次minor gc后就会触发Full GC,是否可调整新生代比例,让jvm多进行minor gc,看是否会减少Full GC次数。首先我先调整了新生代的比例。原来的 NewRatio=1
,逐步调成2 3 4 等,minor gc确实会增多,但是full gc问题还是存在。没有解决问题。
第二次尝试 - 晋升阈值
在进行第一次简单尝试后,开始思考jvm内部的逻辑。
1、对象先进入年轻代
2、经过几次minor gc后,这部分对象没有回收掉
3、晋升到老年代
4、Major gc会回收掉这部分对象
也就是我的目标是要阻止这部分对象晋升到老年代。
使用gc easy分析后,可以很清楚的看到对象的年龄分布。发现确实很多年龄较大的对象(如下图)。
注意要打印年龄分布的话需要加上参数:
-XX:+PrintTenuringDistribution
默认我是-XX:MaxTenuringThreshold=6,不断的调大。
还是会发生full gc,没有解决
第三次尝试 - 晋升阈值 + Survivor区大小
经过第二次尝试,单独提升晋升阈值,会导致对象积攒在Survivor区,从而也会导致过早的晋升到Old区。
所以就开始调整动态阈值和Survivor区的大小。
参数: -XX:SurvivirRatio
需要在二者之间做一个均衡。但是经过几轮的参数调整,并不能降低掉整个Full GC次数。
第四次尝试 - ?
在经过前面的尝试验证后,都不能解决问题。这时该怎么办?首先确认下我们的问题:
在发生Full GC之前,Minor GC并没有很频繁。也就是minor gc次数较少,major gc较频繁。并且老年代的对象很容器被回收掉,是假的老年代对象。
带着这个问题,请教下大师们。登录我的StackOverFlow账号。
提出问题,等待大佬回答。
过一阵有人回复:尝试使用下G1。
于是我这边放弃了"完美"的CMS参数,使用G1。
-XX:+UseG1GC
改完之后,发现真的Full GC消失了。难道大功搞成了?
其实,当我再去 gc easy分析,发现了有很多的Mixed GC,而且吞吐量是相对有下降的。虽然没有Full GC,不会触发Full GC监控告警。其实并没有实质性的解决问题。
问题定位
再次分析堆内存,会发现虽然乍一看,有一部分对象占据空间是比较大的,这部分之所以没有列为我的怀疑目标,是因为这是我故意设置的很大的缓存,并且过期时间设置的比较长。
经过将这部分缓存去除掉之后,会发现问题基本解决掉了。
到此,问题确实找到了。上面其实优化了那么多,其实是在为代码上的错误背黑锅。
代码优化
缓存我使用的Caffeine,缓存大小基本上有600M左右,过期时间6分钟。
如何将这部分数据缓存在堆内存,并且在内存一定的情况下,还要控制gc表现,其实是个问题。为此,我再次登录了我的StackOverFlow账号。
得到Caffeine作者的建议:
GC在这种负载下,表现不会特别好。可能需要加大机器的内存,以及调整G1的年轻代大小。或者,可以使用堆外缓存:OHC 或者 ChronicleMap。
说的很好。但是我选择最简单的办法,减少缓存的大小和过期时间。
观察结果,问题解决。
总结
经过此次的jvm问题,有几个感悟。
1、jvm是最后的手段,首先看代码
2、不要使用网上所谓的”完美“jvm参数
3、垃圾回收器默认不加参数其实很优异,调整单个参数需要结合整体看表现。
文章转载自公众号:凯哥的Java技术活