java OOM 内存溢出问题排查之--一次MAT使用实战

时间:2024-03-28 09:41:20

背景描述

没啥可描述的,就是现场反馈宕机了,小伙伴用MAT分析了一下,说正常,怀疑是现场请求太多了,需要加内存。而我看着这么大一坨都是一个类的实例就不爽,非得研究一下为啥他就那么特殊,没事儿就薅*羊毛,关键还老逮那几只羊身上薅,都给人薅秃了…

定位过程

加载堆转储文件

打开MAT->点击【File】->【Open Heap Dump…】->选择堆转储文件并点击【打开】(或双击)->选择【Leak Suspects Report】(默认选项)->点击【Finish】
PS:如果需要加载的堆转储文件过大,记得调整MemoryAnalyzer.ini中的-Xmx

查看疑似内存泄露

上一步完成后(或点击概览页面正下方的【Leak Suspects】),进入到疑似内存泄露分析页面,该页面最上面以饼状图展示了堆转储文件中的内存分布
正常情况下该图的各区域是相对平均的,如果出现一个或几个明显比例严重失调的色块,那就代表可能出现内存泄露了,如下图

java OOM 内存溢出问题排查之--一次MAT使用实战

MAT还会帮我们自动分析出来疑似泄露的原因,下图就是其自动分析出267个java.lang.Thread实例占据了5,193,146,960个字节(占总内存的96.17%)
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

有的时候到这里就结束了,为啥?因为已经知道哪个对象有问题了。但是这次的示例并没有结束,我们只定位到了Thread实例占用内存较高,但为啥高不知道
不过好歹找到一点儿规律,所有的线程都是通过一个线程池创建出来的,因为线程名称都是qtpxxxx-xxx这种格式(体现出了良好的命名规范是多么重要,一定不要直接new Thread,不然名字你都看不懂–qtp是jetty提供实现了JDK Executor接口的线程池,默认的任务执行容器,全称QueuedThreadPool)

尝试定位疑似内存泄露代码位置

接下来我们打开问题明细,看看是否有啥有用的东西。滚动页面到当前问题最底部,并点击【Details >>】
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

选择我们的对象,并查看当前对象被哪些外部对象引用
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

展开对象列表
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

以保留堆大小(Retained Heap)排序(Retained Heap=对象本身大小与其所引用对象大小之和,理论上是假设该对象被GC成功,能释放多少内存;Shallow Heap=对象自身大小,正常情况下参考意义不大),看占用空间最大的几个对象
PS:数值的单位可以在【Window】->【Preferences】->【Memory Analyzer】->【Bytes Display】选择,默认为byte
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

从下图可以看到线程总数不多,也就300多个,但单个线程占用的内存可不小,最大的占用了122MB,其他的也都平均在70MB以上,但也看不出来内存都哪儿去了
(刚开始还怀疑是否使用ThreadLocal不当造成的内存泄露,后来发现跑偏了,内存被实打实的用着呢)
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

通过支配树(Dominator Tree)分析具体异常对象

选择想要分析的对象,并右键选择【Java Basics】->【Open In Dominator Tree】(可以选择分组,根据自己实际情况选择)
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

下图可以看到我们的对象明显存在异常属性。引用对象总数达到了十多万不说,前面几个对象占用的内存明显过高,单个对象动辄几MB甚至几十MB
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

就拿java.lang.String开刀吧。右键后选择【Copy】->【Save Value To File】->输入文件地址后点击->【Finish】完成转储
PS:问我为啥不直接复制值?太大,复制不下来;又问我为啥不拿最大的net.sf.json.JSONArray试验?复制了,他输出的是对象地址!!!
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

文件存储的是一个json字符串,关键是存储的文本文件就有10MB+
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

格式化看了下,呕吼!几十万行数据,亮瞎了我的钛合金狗眼呀
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

通过追踪异常对象的堆栈信息定位具体方法

点击分析窗格左上角的小齿轮,打开堆栈信息查看窗口
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

从最下面网上追,可以找出来完整的堆栈信息,根上就是jetty的QueuedThreadPool.runJob,而我们只需要关注自己项目里面的最后一个方法即可,就是ZnfzbaxtHttpApi.moveData方法
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

自此我们找到了这些对象都是从哪儿被创建的
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

java OOM 内存溢出问题排查之--一次MAT使用实战

从代码里面我们也可以看到这里的字符串对象,跟咱们最初的那个java.lang.String内容就能对上了。exe方法里面就不在这里详解了,是一个命令模式的具体实现,可以理解为去做具体的事儿了
PS:其他的根据上面的过程自行寻找怀疑点,并跟踪代码逐步定位即可
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

写在最后

首先感谢

@郭锋 @郝正彬 两位前辈,梳理这段代码又让我学到了不少东西,统一上下文还能这样用,新技能get√
PS:是不是以后有卷宗的问题就可以直接找两位请教了???手动狗头!!!

总结的一些想法

AbstractCommand中exe方法的各级调用均使用的JSONObject,那对外接口的params定义成JSONObject的更合适,外部不调用jsonObject.toString,可以节省不少内存占用(从上面几幅图里面可以看到最大的字符串对象占了20MB+的内存,N多个线程就是挺多的了,按比率的话,最少也得节省1/5的内存,按照现状,就是节省1G内存,或许就不会OOM了也说不定哦)
java OOM 内存溢出问题排查之--一次MAT使用实战转存失败重新上传取消

java OOM 内存溢出问题排查之--一次MAT使用实战

世上真的没有银弹!东西不是拿过来就用的,咱们要好好想想别人的东西是否能直接用,甚至是自己的东西过了一段时间后是否还能正常使用?虽然说OOM是因为需要处理的材料太多了,但是如果合理拆分,分批来做还会出现这个问题吗?无非就是重构嘛,再好的架构设计也会被不断变化的环境给淘汰的,我们要懂得跟随时代的脚步不断演进

现场的资源一定要给不能太抠门,起码要有1/3的冗余吧,不然稍微有点儿风吹草动,服务先挂了,那大家就GG了

追加:原来json-lib和其他几个json框架的性能差那么多,而且我们用的还就是json-lib…
从定位过程中的几幅图里,我们可以看到内存的主要消耗都是在JSON处理这里,如果操作效率以10倍提升,出现OOM的概率也会直线下降,看来要过一遍代码,替换过时类型了

java OOM 内存溢出问题排查之--一次MAT使用实战

java OOM 内存溢出问题排查之--一次MAT使用实战

java OOM 内存溢出问题排查之--一次MAT使用实战

PS:MAT的功能很强大,这篇文章才讲到其中的九牛一毛,更多惊喜等你去发现!

最后祝大家:新年快乐,生意兴隆,万事如意,大吉大利!