Java内存分析工具MAT(Memory Analyzer Tool)安装使用实例

1. 前言

生产环境中,一旦出现内存泄漏,长期运行下非常容易引发内存溢出(OutOfMemory,OOM)故障,如果没有一个好的工具提供给开发人员定位问题和分析问题,那么这将会是一场噩梦。为此,JDK提供了一些内存泄漏的分析工具,如jconsole,jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。

2. jmap命令

既然要分析内存,首先需要获取可供分析的原始内存文件,这就需要用到jmap命令。jmap是JDK自带的一种用于生成内存镜像文件的工具,通过该工具,开发人员可以快速生成dump文件。开发人员可以使用命令“jmap -help”查看jmap的常用命令,如下所示:

controller-192-168-1-3:~ # jmap
Usage:
    jmap [option] <pid>
        (to connect to running process)
    jmap [option] <executable <core>
        (to connect to a core file)
    jmap [option] [server_id@]<remote server IP or hostname>
        (to connect to remote debug server)

where <option> is one of:
    <none>               to print same info as Solaris pmap
    -heap                to print java heap summary
    -histo[:live]        to print histogram of java object heap; if the "live"
                         suboption is specified, only count live objects
    -clstats             to print class loader statistics
    -finalizerinfo       to print information on objects awaiting finalization
    -dump:<dump-options> to dump java heap in hprof binary format
                         dump-options:
                           live         dump only live objects; if not specified,
                                        all objects in the heap are dumped.
                           format=b     binary format
                           file=<file>  dump heap to <file>
                         Example: jmap -dump:live,format=b,file=heap.bin <pid>
    -F                   force. Use with -dump:<dump-options> <pid> or -histo
                         to force a heap dump or histogram when <pid> does not
                         respond. The "live" suboption is not supported
                         in this mode.
    -h | -help           to print this help message
    -J<flag>             to pass <flag> directly to the runtime system

在此大家需要注意,jmap工具有一部分命令仅限于Linux和Solaris平台,而Windows平台下能够使用的命令只有“jmap -histo<pid>”和“jmap -dump:<dump-options><pid>”。不过一般来说,使用命令“jmap -dump:<dump-options><pid>”生成dump文件应该是最常用的命令之一,由于生成dump文件时比较耗时的,因此大家需要耐心等待,尤其是大内存镜像生成dump文件则需要耗费更长的时间来完成。

3. MAT工具的下载和安装

MAT(Memory Analyzer Tool)工具是eclipse的一个插件(MAT也可以单独使用),使用起来非常方便,尤其是在分析大内存的dump文件时,可以非常直观的看到各个对象在堆空间中所占用的内存大小、类实例数量、对象引用关系、利用OQL对象查询,以及可以很方便的找出对象GC Roots的相关信息,当然最吸引人的还是能够快速为开发人员生成内存泄露报表,方便定位问题和分析问题。

    MAT工具的下载地址为:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

下载完成后,直接解压,运行其中的MemoryAnalyzer.exe文件即可启动MAT工具,如下所示:

    本文所使用的MAT工具的版本为最新的1.7.0,只要确保机器上装有JDK并配置好相关的环境变量,MAT可正常启动。

4. 使用MAT工具进行内存泄露分析

获取dump文件有两种方法:

  • 其一,通过上面介绍的 jmap工具生成,可以生成任意一个java进程的dump文件;
  • 其二,通过配置JVM参数生成,选项“-XX:+HeapDumpOnOutOfMemoryError ”和-“XX:HeapDumpPath”所代表的含义就是当程序出现OutofMemory时,将会在相应的目录下生成一份dump文件,而如果不指定选项“XX:HeapDumpPath”则在当前目录下生成dump文件。

虽然有两种方式获取dump文件,但是考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT工具是最常见的组合。

    为了演示MAT的使用方法,本文采用jamp生成了一个Java继承的dump文件。

    4.1 Overview选项

当成功启动MAT后,通过菜单选项“File->Open heap dump...”打开指定的dump文件后,将会生成Overview选项,如下所示:

在Overview选项中,以饼状图的形式列举出了程序内存消耗的一些基本信息,其中每一种不同颜色的饼块都代表了不同比例的内存消耗情况。

4.2 Dominator Tree

如果说需要定位内存泄露的代码点,我们可以通过Dominator Tree菜单选项来进行排查。Dominator Tree提供了一个列表。Dominator Tree:对象之间dominator关系树。如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator 。Dominator Tree由系统中复杂的对象图计算而来。从MAT的dominator tree中可以看到占用内存最大的对象以及每个对象的dominator,如下所示:

点开“+”符号,可以进一步查看内层应用情况,同时还可以看到对应类对象的属性值,如下所示:

4.3 Histogram选项

进一步,可以通过Histogram分析,Histogram列出了每个类的实例数量,点击Action下的Histogram,得到以下结果:

如果需要查询特性的某个类,我们可以在第一行输入类名或者关键词进行正则匹配查找,如查找“netty”:

可以看出,查找“netty”输出的结果列表是无序的,如果匹配到的结果很多,查找起来比较困难,因此,我们可以对结果进行排序:选中结果列表的任意一行,鼠标右键-》Colums->Sort By->如Class Name,结果如下:

当我们找到疑似存在泄漏的类之后,我们可以进行进一步分析。比较重要的一点,选中疑似类,右键出来选中List Objects,得到的结果再右键选中"Paths to GC Roots",我们可以通过它快速找到GC ROOT,如果存在GC ROOT,它就不会被回收。

4.4 Path to GC Roots 

查看一个对象到RC Roots的引用链

通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references,意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。 

其它重要选项:​​​​​​​

1. List objects :
with incoming references 引用到该对象的对象
with outcoming references 被该对象引用的对象

2. Show objects by class : 
incoming references 引用到该对象的对象
outcoming references 被该对象引用的对象 


4.5 OQL(Object Query Language)

类似SQL查询语言
Classes:Table
Objects:Rows
Fileds: Cols

select * from com.example.mat.Listener

查找size=0并且未使用过的ArrayList
select * from java.util.ArrayList where size=0 and modCount=0

查找所有的Activity
select * from instanceof android.app.Activity

4.6  利用Histogram和Dominator Tree分析内存泄露

在分析内存泄露时,必须要掌握粒度,所谓粒度就是你此刻dump的hprof文件究竟是分析谁的泄露,如果你在开始前心中没有个目标,最后取出来的hprof也分析不出什么原因。粒度越小,对你分析问题也就越有利,当你把一个个小粒度问题解决后,整个App的泄露就迎刃而解了。也许这么说,大家心中有点迷糊。下面就举例来说吧:

 假如现在有个项目包含Module几十个,每个Module包含的Activity数以百计,现在让你分析它是否内存泄露,如果你只是胡乱抓个hprof根本分析不出什么。假如你就针对某个Activity分析这样问题就简单多了。比如你现在分析ActivityA的内存泄露问题,你可以参考如下步骤:

  •  Step1:进入ActivityA之前,你先dump个hprof文件HprofA;
  • Step2:进入ActivityA操作一会,再退出ActivityA后dump个hprof文件HprofB;
  • Step3:采用Histogram和Dominator Tree对比分析这两个Hprof文件,即可得出ActivityA是否泄露

现在以分析TestActivity为例,按上述步骤实战分析,先抓取进入TestActivity前后的hprof文件,按如下步骤对比两个hprof的异同,如下图1,2:

图1 选择所需比较的hprof

图2 比较两个hprof

正如图2所示,易知在执行进出TestActivity后,多出了个TestActivity对象,按理论上来说在进入Activity后会创建个Activity,但是按Back键返回后这个Activity就会被销毁进而从Task栈上被移除,也就是说这个操作前后不应该会多出个Activity,因此可以断定TestActivity存在泄漏。

TestActivity存在泄漏,那我们应该怎么解决呢?因此我们就需要找到为何泄漏,为什么本该销毁的Activity却没有被销毁?如知真相如何,请看下图3-4

图3 获取TestActivity的Reference chain

图4 TestActivity的引用关系

从图4易知TestActivity没有被释放就是因为GC Root(TestActivity$1)引用着TestActivity,到此原因也一目了然。找到了只是开始,解决才是关键。这时让我们查看下TestActivity代码:

public class TestActivity extends Activity {   
    private static final Object mLock = new Object(); 
    @Override
    protected void onCreate(Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);
        DebugUtil.StrictModeDebug();
        setContentView(R.layout.test_main);   
        new Thread(){//匿名线程
            public void run() {
                synchronized (mLock) {
                    try {
                        mLock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

从代码上可以发现TestActivity里存在个匿名线程,且一直处于等待状态,直到退出TestActivity仍未被唤醒,进而导致该线程就一直没有结束,它所持有的TestActivity也就无法被释放了(可能大家听到此处会很疑惑,线程没有结束可以理解,但是它并没有持有TestActivity呀?我只能说是隐含this,如还不明白,请自行参阅java内部类相关内容),如要解决此泄露,只需在Activity的onDestory里将线程唤醒让其可以正常结束就OK了。

优化建议

  1. 使用线程时,一定要确保线程在周期性对象(如Activity)销毁时能正常结束,如能正常结束,但是Activity销毁后还需执行一段时间,也可能造成泄露,此时可采用WeakReference方法来解决,另外在使用Handler的时候,如存在Delay操作,也可以采用WeakReference;
  2. 使用Handler + HandlerThread时,记住在周期性对象销毁时调用looper.quit()方法;
  3. 建议少使用匿名类或内部类,可考虑使用嵌套类(带static那种类),减少对周期性对象的隐性持有;

​​​​​​​

5. 推荐一本书

最近读了一本书——《服务端开发:技术、方法与实用解决方案》,大为受用,在此推荐给大家。该书作者曾获得阿里第二届技术讲师课程大赛年度冠军(由阿里 CPO 和 CSDN CEO 共同颁奖),在阿里和蚂蚁内部所授技术课程口碑甚好。全书分 14 章,约 30 万字,正文 373 页,共 389 页,由机械工业出版社出版发行。内容分为上下两篇。上篇 1~6 章,主题为技术和方法;下篇 7~14章,主题为解决方案。书中列举了大量实用案例,为了便于读者理解,作者还绘制了超过 200 幅插图,可谓图文并茂。

在这里插入图片描述

5.1 读者对象

  • IT 从业人员:服务端开发工程师、客户端开发工程师、产品经理、测试开发工程师等。
  • 高校学生:计算机、软件、自动化、电气、通信等专业有志于进入 IT 行业的在校学生。

5.2 该书特色

无论技术如何演进,其背后的方法往往大同小异,经典解决方案历久弥新。因此,掌握方法和实用解决方案尤为必要。不同于一般的 IT 技术书籍,这本书不局限于任何一种具体的编程语言、框架、容器、中间件或编程思想,而是致力于全景式、体系化地解读服务端开发的流程和重难点。

该书共 14 章,内容从逻辑上可以分为两大部分。

第一部分为第 1~6 章,主题是技术和方法。首先概述服务端开发的职责、技术栈、核心流程和进阶路径,然后从需求分析、抽象建模、系统设计、数据设计、非功能性设计 5 个方面逐一展开,结合案例深入解读服务端开发的实操方法和重难点,为读者清晰呈现服务端开发的全景图。通过学习本篇内容,读者可以快速、体系化地掌握服务端开发的相关知识和方法。

第二部分为第 7~14 章,主题是解决方案。针对高并发、高可用、高性能、缓存、幂等、数据一致性等服务端开发的典型问题,结合业务场景进行系统性分析并给出解决方案。此外,就接口设计、日志打印、异常处理、代码编写、代码注释等实施细节给出行业案例和规范。本篇内容如同一本服务端开发问题手册,当读者在实践中遇到问题时,可以从中查找解决方案。

5.3 购买渠道

目前,本书已经在京东、淘宝、当当、拼多多等电商平台发售。在电商 APP 搜索关键词 “服务端开发”、“服务端开发技术”,即可搜索到该书。

  • 13
    点赞
  • 125
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jin_Kwok

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值