1.1.1.1发现和调查问题 找出问题的原因
问题的原因是我们收到了jdos容器CPU的报警,CPU的利用率已经达到了104%
观察机器日志发现,此时有许多线程在执行运行批准任务。通常,运行批准任务是低CPU和高内存类型,因此考虑到FulllGC引起的大量CPU占用(以前也有类似情况,通知用户重启应用程序后解决问题)。
通过泰山查看机器内存的使用情况:
可见CPU利用率确实偏高,**但内存利用率并不高,只有62%,**属于正常范围。
其实这里有点迷茫。按道理来说,这个时候内存应该已经满了。
后来,根据其他指标,如突然进入流量,也怀疑jsf界面是由突然大量调用引起的cpu占据的,因此内存利用率不高,但后来慢慢排除。事实上,这里有点无助。这种现象与猜测不一致。只有CPU增长而没有内存增长。**单方面CPU增长的原因是什么?**然后,在这个方向调查了很长一段时间后,它被否认了。
突然意识到监控会有“问题”吗?
换句话说,我们看到的监控应该有问题。这里的监控是机器监控,而不是JVM监控!
JVM使用的CPU可以反映在机器上,而JVM的高堆内存在机器上并不明显。
然后去sgm查看相应节点的jvm相关信息:
可以看出,我们的堆内存在老年时代确实被打满然后清理干净,此时的CPU使用也可以与GC时间相对应。
此时可以确定是Fulll GC引起的问题。1.2 找FULL GC的原因
首先,dump发出了gc前后堆内存快照,
然后使用JPofiler进行内存分析。(JProfiler是一种堆内存分析工具,可以直接连接在线jvm实时查看相关信息,也可以分析dump的堆内存快照,分析某一时刻的堆内存。)
首先,解压我们的dump文件,修改后缀名称.bin,然后打开。(我们可以使用行云上自带的dump小工具,也可以通过命令手动dump文件去机器)
首先,选择Biggesttttt首先选择 Objects,查看当时堆内存中最大的对象。
从图中可以看出,四个List对象占据了近900MB的内存,我们刚刚看到最大的堆内存只有1.3GB,因此,再加上其他对象,很容易将老年人占满,导致fulll gc问题。
选择最大的对象之一作为我们想要查看的对象
此时,我们已经可以定位到相应的大内存对象对应的位置:
事实上,到目前为止,我们已经能够大致定位问题。如果仍然不确定,我们可以查看具体的对象信息。方法如下:
我们可以看到我们的大List对象,其实里面有很多Map对象,每个Map对象都有很多键值对。
Map中的相关属性信息也可以在这里看到。
相关信息也可以直接在以下界面看到:
然后一路点击,可以看到相应的属性。
到目前为止,我们已经在代码中找到了大对象的位置。二、解决问题2.1 在代码中找到大对象的位置和问题的根本原因
首先,我们根据上述过程找到相应的位置和逻辑p>
我们项目的逻辑是这样的: 首先,我们将分析用户上传的Excel样本,并将其加载到内存中作为List变量,即我们在上面看到的变量。一个20w的样本,此时字段数量为a,占用空间约100mb。 然后根据用户样本中的数据,在用户样本中添加一些额外的请求数据,并根据该数据请求的相关结果。此时,字段数量为a+n,占用空间约为200mb。 循环完成后,将200mb的数据存储在缓存中。 开始生成excel,从缓存中取出200mb数据,并根据之前记录的a字段将初始样本字段取出填充excel。
用流程图表示:
结合一些具体调查问题的图片:
其中一个现象是,每个gc后的最小内存正在逐渐扩大,与上述步骤中的第二步相对应,内存正在逐渐扩大。
结论:
将用户上传的excel样本加载到内存中,并将其作为一个List
这里列出了为什么使用Hashmap会导致内存膨胀,主要原因是存储空间效率低下:
在Hashmap中计算一个Long对象的内存:
然后这两个Long对象组成Mapp.Entry之后,又增加了16字节的对象头(8字节MarkWord+8字节Klass指针=16字节),然后是8字节next字段和4字节inthash字段(8字节next指针+4字节hash字段+4字节填充=16字节)。最后,在Hashmap中引用了Entry的8字节,增加了两个长整形数字,实际消耗的内存是(Long(24byte)×2)+Entry(32byte)+HashMapRef(8byte)=88byte,空间效率是除以所有内存空间的有效数据,即16字节/88字节=18%。
——5.2对Java虚拟机的深入理解.6
以下是刚上传的excel中dump的堆内存对象,占用的内存达到128mb,而上传的excel实际上只有17.11mb。
空间效率17.1mb/128mb≈13.4%2.2 如何解决这个问题
暂时不讨论上述过程是否合理,解决方案一般可分为两类,一类是根本原因,即不将对象放入jvm内存中,而是存储在缓存中,而不是内存中,大对象问题自然很容易解决。另一种是症状治疗,即缩小大内存对象,使其在日常使用场景中一般不会触发频繁的fulll gc问题。
两种方法各有优缺点:2.2.1 激进治疗:不要将他存入内存
解决逻辑也很简单。例如,在加载数据时,将样本加载数据逐个存储在redis缓存中,然后我们只需要知道样本中有多少数量,并根据数量的顺序从缓存中取出数据来解决这个问题。
优点:这个问题可以从根本上解决,以后基本不存在。无论数据量有多大,只需添加相应的redis资源即可。
缺点:首先会增加很多redis缓存空间的消耗。其次,考虑到显示,对于我们的项目来说,这里的代码是古老而晦涩的,需要大量的工作量和回归测试来改变。
2.2.2 保守治疗:减少其数据量
分析2.1上述流程,第三步是完全不必要的,先存储缓存再取出,占用额外的缓存空间。(猜测是历史问题,这里不再深入研究)。
其次,在第二步中,多余的字段n在请求结束后无用,因此可以考虑在请求结束后删除无用字段。
此时,还有两种解决方案,一种是只删除无用字段来减少其map的大小,然后将其作为参数传输给生成excel;另一种方法是要求直接删除map,然后在生成excel时重新读取用户上传的excel样本。
优点:变化小,不需要太复杂的回归测试
缺点:在极端大数据量的情况下,fulll仍然可能出现 gc的情况
具体实现方式不展开。
其中一种实现方式 ///获取有用的字段String[] colEnNames = (String[]) colNameMap.get(Constant.BATCH_COL_EN_NAMES);List
首先,本文中的监控图是在复制当时场景时人工制作的GC。
在cpu利用率图中,我们可以观察到cpu利用率的上升时间确实与gc时间一致,但当时场景中没有104%的cpu利用率。
事实上,直接原因相对简单,因为虽然系统出现了fulll gc,但并没有频繁出现。
小范围低频fulll gc不会导致系统的cpu飙升,这也是我们看到的现象。
那当时的场景是什么原因呢?
正如我们上面提到的,堆内存中的大对象会随着任务的进行逐渐扩展,所以当我们有足够的任务和足够长的时间时,每次full都可能导致 GC后的可用空间越来越小。当可用空间小到一定程度时,每次fulll gc完成后,发现空间仍不足,会触发下一个gc,导致gc频繁发生最终结果,导致cpu频率飙升。四、问题排查总结 当我们遇到在线cpu利用率过高时,我们可以首先检查它是否是fulll gc引起的问题,注意jvm监控,或使用jstat相关命令。不要被机器内存监控误导。 如果确定是gc引起的问题,可以通过JProfiler直接在线jvm或使用dump保存堆快照进行离线分析。 首先,你可以找到最大的对象,通常是由大对象引起的fulll gc。还有一种情况,不像这样明显是四个大对象,也可能是十几个50mb比较平衡的对象,具体情况需要具体分析。 通过上述工具找到问题对象,找到堆栈对应的代码位置,通过代码分析找到问题的具体原因,通过其他现象推断猜测是否正确,然后找到问题的真正原因。 根据问题的原因来解决这个问题。
当然,以上只是一个不太复杂的调查情况。不同的系统必须有不同的内存。我们应该分析具体的问题,从这个问题中我们可以学到的是如果我们调查和解决问题的想法。