如何使用内存分析工具定位内存泄露?

您所在的位置:网站首页 前端内存泄露如何解决 如何使用内存分析工具定位内存泄露?

如何使用内存分析工具定位内存泄露?

2022-03-26 11:57| 来源: 网络整理| 查看: 265

 

如何使用内存分析工具定位内存泄露?_内存泄露

 

如何使用内存分析工具定位内存泄露?_内存泄露_02

本文以我司生产环境Java应用内存泄露为案例进行分析,讲解如何使用Eclipse的MAT分析定位问题

如何使用内存分析工具定位内存泄露?_内存泄露_03

 

一. 背景

 

11月10号晚上8点收到报警邮件,一看是OOM

 

打开公司监控系统查看应用各项指标发现JVM中老年代在持续增长(从上次发布10月30号到11月10号的12天内一直在增长, 存在内存泄露迹象)

如何使用内存分析工具定位内存泄露?_内存泄露_04

 

从图中可以看出, 从10月30号发布到11月10号oom期间11天老年代一直在缓慢上涨, 虽然有下降, 但整体趋势是上升的,平均每天泄露约50M内存, 说明每次都无法完全释放干净

 

因为生产环境的JVM添加了 -XX:+HeapDumpOnOutOfMemoryError 参数,该配置会把dump文件的快照保存下来供后续分析排查问题,也可以使用jmap或jcmd等jvm命令进行dump:

 

jmap -dump:format=b,file=文件名 [pid]jcmd pid GC.heap_dump 文件路径

 

 

二. 分析内存泄露

 

内存泄露和内存溢出的区别:内存泄露从老年代的增长情况看是缓慢上升的, 最终达到老年代上限才会导致溢出,有些内存泄露可能需要很长的时间发生, 所以说内存泄露更隐蔽, 不像内存溢出那样容易暴露(内存溢出直接抛出OOM), 而且内存长时间得不到释放会导致服务性能越来越差、gc时间变长、响应变慢:

如何使用内存分析工具定位内存泄露?_内存泄露_05

 

从图中可以看出在12天里每天大概泄露(增长) 50M 左右, 这种情况下定位泄露原因需要多次dump采集样本, 然后和上次的比较分析, 即需要多个dump文件进行比较分析才能精确定位问题。 否则很难看出具体泄露的点, 加上dump文件中大部分是正常的内存使用, 会干扰问题的定位, 增加排查难度。

 

所以当时的做法是每天固定时间dump一次, 采集足够多的样本, 如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_06

 

另外测试环境不好重现的主要原因是不清楚是哪个接口调用引起的, 这个Java服务有多个暴露的api, 而且测试环境不方便压测,压测量大了, 底层接口熔断, 压测量小看不出泄露迹象, 所以得从dump分析入手, 找到问题所在再去测试环境验证。

 

这里使用Eclipse的memory analysis tool(MAT)工具进行分析

 

把下载到本地的多个dump文件用mat依次打开("File → Open Heap Dump"), 如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_07

 

比如我们要分析这3个dump文件(当然你也可以分析更多个, 这样会更精准), 打开后, 使用compare basket功能找出内存泄露的差异点:

 

1. 使用 compare basket 功能分析内存泄露

 

1> 菜单栏 window → compare basket ,打开比较窗口(如果最下面一栏已经有compare basket则这步不需要),如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_08

 

2> 依次打开3个dump的dashboard面板, 在下方的 Actions一栏点击"histogram"或"dominator tree"生成对应的直方图或支配树列表,如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_09

 

直方图或支配树都可以列出堆中存活的所有对象,但二者的维度不同, 直方图按照类型统计, 支配树是以对象维度统计。

 

如果你对项目代码比较熟悉, 通过直方图定位内存泄露会更快,因为它是按照类型全部平铺开的,如果这个项目不是你负责的, 建议使用支配树的方式, 因为支配树包含了对象之间的引用关系(支配树视图可以展开查看内部引用层级)

 

3> 我们以支配树做比对, 在最下面一栏的"Navigation History (window → navigation history)"里(直方图类似)找到在第2步打开的支配树dominator tree图标, 右键添加到compare basket, 如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_10

 

4> 重复上面的2, 3步骤依次把其他的dump文件添加到"compare basket"栏, 然后点击右上角的红色感叹号, 生成比较结果,如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_11

(注意比较的dump文件的顺序,时间最早的在上面,可以通过右上角的上箭头↑和下箭头↓调整顺序)

 

生成的比对结果如下:

如何使用内存分析工具定位内存泄露?_内存泄露_12

(可以放大查看)

 

Shallow Heap一列后面的序号 #0, #1, #2 分别对应:

第一个dump文件占用的shallow size, 第二个dump文件占用的shallow size , 第三个dump文件占用的shallow size

 

Retained Heap #0, Retained Heap #1, Retained Heap #2 这3列分别对应:

第一个dump文件占用的retained size, 第二个dump文件占用的retained size , 第三个dump文件占用的retained size

 

通过Retained Heap的变化趋势可以看出:

红框 圈出的是内存连续增长的对象, 可以通过右边红框的retained heap看出内存变大的趋势

绿框 圈出的是没有变化的对象(至少在这3次比较中没有变化)

蓝框 圈出的是内存占用下降的对象

 

一般我们主要关注红框标出的对象, 因为这部分发生内存泄露的嫌疑最大

 

这里先区分两个概念:

Shallow Size

对象自身占用的内存大小,不包括它引用的对象。

针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。

针对数组类型的对象,它的大小是数组元素对象的大小总和。

 

Retained Size

Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)

Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。

 

 

因为这里我们比较的是支配树, 所以按照retained heap倒序排列, 从左到右依次为: retained heap #0 → retained heap #1 → retained heap #2(以最后一个retained heap #2 倒序, 因为这个是最后一次dump的内存快照, 这样可以看出内存泄露的增长趋势)

 

2. 定位内存泄露

 

基于上一步得出的比较结果, 可以看出"org.apache.tomcat.util.threads.TaskThread http-nio-8080-exec-*" 有内存泄露的嫌疑, 查看它的引用关系:

如何使用内存分析工具定位内存泄露?_内存泄露_13

 

点击"with outgoing references"后逐层展开第一个对象内部的引用关系(以Retained Heap倒序,主要是看retained size排在前面的对象), 如下:

如何使用内存分析工具定位内存泄露?_内存泄露_14

 

可以看到TaskThead内部有一个threadLocal, threadLocal内部有一个concurrentHashMap,这个map里存的是我们的日志相关对象"com.*.framework.log.FieldAppendedValue",从下面几个map里的key可以确定是我们记录到日志系统(ElasticSearch)的对象, 这些日志对象主要记录调用接口的请求报文、响应报文、SOA接口名称等信息,如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_15

 

但为什么日志对象会占用这么多内存?而且这里看到的只是其中一个taskThread里,继续展开RESPONSE_CONTENT的val对象FieldAppendedValue内部引用, 如下:

如何使用内存分析工具定位内存泄露?_内存泄露_16

 

发现FieldAppendedValue内部维护了一个CopyOnWriteArrayList对象, 这个list里竟然存放了10674个值,正常来讲不可能一次接口请求会有这么多的日志对象, 而且接口请求完记录到ES后, 这部分内存就应该释放了才对。

 

查看CopyOnWriteArrayList内部存储的内容,如下:

如何使用内存分析工具定位内存泄露?_内存泄露_17

 

随便打开10675个中的几个FieldAppendedValue, 发现内部存放的都是同一个接口的请求响应报文,如下图:

如何使用内存分析工具定位内存泄露?_内存泄露_18

 

可以右键copy→ value 把值复制出来查看, 接口报文如下:(响应报文)

{    "ResponseStatus": {        "Timestamp": "/Date(1605583909438+0800)/",        "Ack": "Success",        "Errors": [],        "Build": null,        "Version": null,        "Extension": []    },    "downloadUrl": "https://ii066.cn/hFGBEW"}

 

从上面那张concurrentHashMap截图(key : SOA_METHOD_NAME) 可知这个接口名是: getDownloadLink, 也就是说list里10675个日志对象存的都是"getDownloadLink"这个接口的报文。而且这只是其中一个TaskThead内部情况, 加上全部20个对象, 20 * 10675 大概是213500个接口报文,如下图:

 

如何使用内存分析工具定位内存泄露?_内存泄露_19

 

这个接口是什么鬼?

如何使用内存分析工具定位内存泄露?_内存泄露_20

 

3. 代码分析

 

查看代码得知这个接口并没什么幺蛾子,只是当时的开发同学在调用这个底层接口时新接入了我们部门封装的SOA组件公共类:AbstractSimpleHandler.java(这个公共类主要是通过模板方法在调用接口时记录报文日志埋点、超时时间设置、mock等功能)

 

这次出现OOM的这个Java项目之前调用soa接口是自己实现了一套公共方法(早于框架之前实现), 也就是说只有这一个接口使用了新的公共类AbstractSimpleHandler,其他的接口调用方式还是原来的方式。

 

新的工具类AbstractSimpleHandler记录接口报文的代码是通过调用ELKLogUtils.write()实现的, 这个方法的内部大致逻辑如下:

 

Object value = HttpContext.get(BEHAVIOR_LOG);        if (value == null) {            value = new ConcurrentHashMap();            HttpContext.add(BEHAVIOR_LOG, value);        }

 

HttpContext内部维护的是一个ThreadLocal:

 

public class HttpContext {    private static final int CONTEXT_DEFAULT_SIZE = 1 


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3