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并且未使用过的ArrayListselect * 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代码:
1 | public class TestActivity extends Activity { private static final Object mLock = new Object(); 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了。
优化建议
- 使用线程时,一定要确保线程在周期性对象(如Activity)销毁时能正常结束,如能正常结束,但是Activity销毁后还需执行一段时间,也可能造成泄露,此时可采用WeakReference方法来解决,另外在使用Handler的时候,如存在Delay操作,也可以采用WeakReference;
- 使用Handler + HandlerThread时,记住在周期性对象销毁时调用looper.quit()方法;
- 建议少使用匿名类或内部类,可考虑使用嵌套类(带static那种类),减少对周期性对象的隐性持有;