作者:京东科技 韩国凯
一、问题发现与排查
1.1 找到问题起因
问题起因是咱们收到了jdos的容器CPU告警,CPU使用率曾经达到104%
察看该机器日志发现,此时有很多线程在执行跑批工作。失常来说,跑批工作是低CPU高内存型,所以此时思考是FullGC引起的大量CPU占用(之前有相似状况,告知用户后重启利用后解决问题)。
通过泰山查看该机器内存应用状况:
能够看到CPU的确使用率偏高,然而内存使用率并不高,只有62%,属于失常范畴内。
到这里其实就有点蛊惑了,按情理来说此时内存应该曾经打满才对。
前面依据其余指标,例如流量的忽然进入也狐疑过是jsf接口被忽然大量调用导致的cpu占满,所以内存使用率不高,不过前面都缓缓排除了。其实在这里就有点束手无策了,景象与猜想不符,只有CPU增长而没有内存增长,那么什么起因会导致单方面CPU增长?而后又朝这个方向排查了半天也都被否定了。
前面忽然意识到,会不会是监控有“问题”?
换句话说应该是咱们看到的监控有问题,这里的监控是机器的监控,而不是JVM的监控!
JVM的应用的CPU是在机器上能体现进去的,而JVM的堆内存高额应用之后在机器上体现的并不是很显著。
遂去sgm查看对应节点的jvm相干状况:
能够看到咱们的堆内存老年代的确有过被打满而后又清理后的状况,查看此时的CPU应用状况也能够与GC工夫对应上。
那么此时能够确定,是Full GC引起的问题。
1.2 找到FULL GC的起因
咱们首先dump出了gc前后的堆内存快照,
而后应用JPofiler进行内存剖析。(JProfiler是一款堆内存剖析工具,能够间接连接线上jvm实时查看相干信息,也能够剖析dump进去的堆内存快照,对某一时刻的堆内存状况进行剖析)
首先将咱们dump进去的文件解压,批改后缀名.bin
,而后关上即可。(咱们应用行云上自带的dump小工具,也能够本人去机器上通过命令手工dump文件)
首先抉择Biggest Objects,查看过后堆内存中最大的几个对象。
从图中能够看出,四个List对象就占据了近900MB的内存,而咱们刚刚看到堆内存最大也只有1.3GB,因而再加上其余的对象,很容易就会把老年代占满引发full gc的问题。
抉择其中一个最大的对象作为咱们要查看的对象
这个时候咱们曾经能够定位到对应的大内存对象对应的地位:
其实至此咱们曾经可能大略定位出问题所在,如果还是不确定的话,能够查看具体的对象信息,办法如下:
能够看到咱们的大List对象,其实外部是很多个Map对象,而每个Map对象中又有很多键值对。
在这里也能够看到Map中的相干属性信息。
也能够在以下界面间接看到相干信息:
而后一路点上来就能够看到对应的属性。
至此,咱们实践上曾经找到了大对象在代码中的地位。
二、问题解决
2.1 找到大对象在代码中的地位与问题的根本原因
首先咱们根据上述过程找到对应地位与逻辑
咱们的我的项目中大略逻辑是这样的:
- 首先会解析用户上传的Excel样本,并将其加载到内存中作为一个List变量,即咱们上述看到的变量。一个20w的样本,此时字段数量有a个,大略占用空间100mb左右。
- 而后遍历循环用户样本,依据用户样本中的数据,再减少一些额定的申请数据,依据此数据申请相干后果。此时字段数量有a+n个,占用空间曾经在200mb左右。
- 循环实现后将此200mb的数据存入缓存。
- 开始生成excel,将200mb数据从缓存中取出,并依据之前记录的a个字段,取出初始的样本字段填充至excel。
用流程图示意为:
联合一些具体排查问题的图片:
其中一个景象是每次gc后的最小内存正在逐渐变大,对应上述步骤中第二步,内存正在逐渐收缩。
论断:
将用户上传的excel样本加载到内存中,并将其作为一个List<Map<String, String>>
的构造存储起来,首先一个20mb的excel文件以此形式存储会收缩占用120mb左右堆内存,此步骤会大量占用堆内存,并且因为工作逻辑起因,该大对象内存会在jvm中存在长达4-12小时之久,导致一但工作过多,jvm堆内存很容易被打满。
这里列举了为什么应用HashMap会导致内存收缩,其次要起因是存储空间效率比拟低:
一个Long对象占内存计算:在HashMap<Long,Long>构造中,只有Key和Value所寄存的两个长整型数据是无效数据,共16字节(2×8字节)。这两个长整型数据包装成java.lang.Long对象之后,就别离具备8字节的MarkWord、8字节的Klass指针,再加8字节存储数据的long值(一个包装对象占24字节)。
而后这2个Long对象组成Map.Entry之后,又多了16字节的对象头(8字节MarkWord+8字节Klass指针=16字节),而后一个8字节的next字段和4字节的int型的hash字段(8字节next指针+4字节hash字段+4字节填充=16字节),为了对齐,还必须增加4字节的空白填充,最初还有HashMap中对这个Entry的8字节的援用,这样减少两个长整型数字,理论消耗的内存为(Long(24byte)×2)+Entry(32byte)+HashMapRef(8byte)=88byte,空间效率为无效数据除以全副内存空间,即16字节/88字节=18%。
——《深刻了解Java虚拟机》5.2.6
以下是刚上传的excel中dump出的堆内存对象,其占用的内存达到了128mb,而上传的excel理论只有17.11mb。
空间效率17.1mb/128mb≈13.4%
2.2 如何解决此问题
暂且不探讨上述流程是否正当,解决办法个别能够分为两类,一类是治标,即不把该对象放入jvm内存中,转而存入缓存中,不在内存中则大对象问题天然迎刃而解。另一类是治本,即放大该大内存对象,在日常应用场景下使其个别不会触发频繁的full gc问题。
两种形式各有优劣:
2.2.1 激进医治:不把他存入内存
解决逻辑也很简略,例如在加载数据时,将其依照样本加载数据一条一条存入redis缓存,而后咱们只须要晓得样本中有多少的数量,依照数量的先后顺序从缓存中取出数据,即可解决该问题。
长处:能够从根本上解决此问题,当前基本上不会存在该问题,数据量再大只须要增加相应的redis资源即可。
毛病:首先会减少许多redis缓存空间耗费,其次从显示思考对于咱们我的项目来说,此处代码古老且艰涩难懂,改变须要较大工作量与回归测试。
2.2.2 激进医治:缩减其数据量
剖析2.1的上述流程,首先第三步是齐全没必要的,先存入缓存再取出,额定占用缓存空间。(猜想系历史问题,此处不再深究)。
其次是在第二步中,多进去的字段n,在申请完结后该字段就曾经无用了,因而能够思考在申请完结后删除无用字段。
此时也有两种解决方案,一种是只删除无用字段缩减其map大小,而后将其作为参数传递给生成excel应用;另一种形式是申请实现间接删除该map,而后在生成excel时再从新读取用户上传的excel样本。
长处:改变较小,不须要太简单的回归测试
毛病:在极其大数据量状况下,仍有可能呈现full gc的状况
具体实现形式就不开展了。
其中一种实现形式
//获取有用的字段
String[] colEnNames = (String[]) colNameMap.get(Constant.BATCH_COL_EN_NAMES);
List<String> colList = Arrays.asList(colEnNames);
//去除无用的字段
param.keySet().removeIf(key -> !colList.contains(key));
三、拓展思考
首先本文中监控图是在复现过后场景时人为制作的gc常见。
在cpu使用率图中,大家能够察看到cpu使用率上升时间的确跟gc的工夫相吻合,然而并没有呈现过后场景中的104%的CPU使用率。
其实间接起因比较简单,就是因为零碎尽管呈现了full gc,然而并没有频繁呈现。
小范畴低频率的full gc不太会引起零碎的cpu飙升,这也是咱们所看到的景象。
那么过后的场景是什么起因呢?
咱们上文提到过,咱们在堆内存中的大对象是会随着工作的进行逐渐收缩的,那么当咱们的工作足够多,工夫足够长,就有可能导致每次full gc后可用空间变得越来越小,当可用空间小到肯定水平之后就,每次full gc实现之后发现空间还是不够应用,就会触发下一次的gc,从而导致最终后果的频繁产生gc,引起cpu频率的飙升不下。
四、问题排查总结
- 当咱们遇到线上cpu使用率过高的状况时,能够先查看是否是full gc引起的问题,留神要看的是jvm的监控,或者应用jstat相干命令查看。不要被机器内存监控所误导。
- 如果确定是gc引起的问题,能够通过JProfiler直连线上jvm或者应用dump保留堆快照后离线剖析。
- 首先能够找到最大的对象,个别状况下是大对象引起的full gc。还有一种状况是,不像这么显著是四个大对象,也可能是比拟平衡的十几个50mb的对象,具体情况还须要具体分析。
- 通过上述工具找到确定有问题的对象后找到其堆栈对应的代码地位,通过代码剖析找到问题的具体起因,通过其余景象推演猜想是否正确,进而找到问题的真正起因。
- 依据问题的起因解决此问题。
当然,上述只是不算很简单的排查状况,不同的零碎必定有不同的内存状况,咱们该当具体问题具体分析,而从此次问题中能够学到的就是如果排查解决问题的思路。