概述
使用MAT工具检测内存泄露问题
Loy.ouyang
一、内存泄露的定义
Android 依靠GC(GarbageCollection,垃圾回收)来回收掉一些不再使用的对象,但是GC并不能防止出现内存泄露。对于那些GCRoot可达(仍然存在引用的对象)且无用的对象,GC就无法对其回收,因此造成内存泄露。对于Android这种内存小的设备,内存泄露会严重影响APP体验。
GC机制我就不详细展出来了,简书的这篇还算详细:
https://www.jianshu.com/p/5db05db4f5ab
二、使用Eclipse MemoryAnalyzer(MAT)工具分析内存泄露
1、获取MAT工具
MAT工具下载网址:
http://www.eclipse.org/mat/downloads.php
解压后,直接双击打开MemoryAnalyzer.exe即可打开MAT。
2、获取.hprof文件
在使用工具之前,我们使用Android studio生成.hprof文件:
打开DDMS界面,操作步骤:
1、在左侧面板中选择你要观察的应用程序进程;
2、点击Update Heap按钮;
3、接着在右侧面板中点击Heap标签,之后操作app,不停地点击Cause GC按钮来实时地观察应用程序内存的使用情况即可;
4、点击DumpHprof files按钮,选择目录存放.hpfor文件
具体如下图:
下面我们来模拟一种Activity内存泄漏的场景,内部类相信大家都有用过,如果我们在一个类中又定义了一个非静态的内部类,那么这个内部类就会持有外部类的引用。现在我在com.android.calculator2.Calculator.java程序中加入一段代码:
...
private class LeakClassextends Thread{
@Override
public void run() {
while (true) {
Log.e("Calculator","ouyang");
}
}
}
@Override
protected void onCreate(BundlesavedInstanceState) {
LeakClass leakClass = new LeakClass();
leakClass.start();
...
}
...
如果说内部类的存活时间不超过类的存活期,是不会出现泄露,但是如上代码所示,显然内部类一直都存活(死循环),这样就会导致LeakClass一直持有Calculator(Activity)的引用,Calculator就无法释放,从而导致内存泄露。
我们运行程序,如果一直来回切换Calculator,就会导致前面创建的Activity一直无法释放,那么长时间操作下我们的应用程序所占用的内存就会越来越高,最终出现OutOfMemoryError。
运行程序后,并来回切换主界面,几次之后,然后按下Cause GC(这步一定要做,避免一些可以GC掉的对象因为未GC而扰乱我们的分析),然后点击Dump Hprof files,获取.hprof文件,这个文件记录着我们应用程序内部的所有数据。我把.hprof文件存放在(随意位置都行):
C:Usersloy.ouyangDesktopmat分析hprofoldcom.android.calculator2.hprof
但是目前MAT还是无法打开这个文件的,我们还需要将这个但是MAT无法直接打开这个.hprof文件,我们需要使用AndroidSDKSdkplatform-tools中的hprof-conv.exe工具将HPROF文件从Dalvik格式转换成J2SE格式。打开cmd终端,使用hprof-conv命令就可以完成转换工作,如下所示:
hprof-convC:Usersloy.ouyangDesktopmat分析hprofoldcom.android.calculator2.hprofC:Usersloy.ouyangDesktopmat分析hprofoutcom.android.calculator2.hprof
前一个路径是DDMS生成的.hprof文件,后面的路劲是转化后的.hprof文件路径。
打开MAT工具,点击File->OpenFile…,找到
C:Usersloy.ouyangDesktopmat分析hprofoutcom.android.calculator2.hprof
然后加载com.android.calculator2.hprof文件,最后点击finish,加载完成。
如果没有出来overview界面,就点击最左边的restore按钮展开overview界面。
MAT中提供了非常多的功能,这里我们只要学习几个最常用的就可以了。上图最中央的那个饼状图展示了最大的几个对象所占内存的比例,这张图中提供的内容并不多,我们可以忽略它。在这个饼状图下就有几个非常有用的工具了,我们来学习一下。
Histogram可以列出内存中每个对象的名字、数量以及大小。
Dominator Tree会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。
一般最常用的就是以上两个功能了,那么我们先从DominatorTree开始学起。
现在点击Dominator Tree,结果如下图所示:
1. 最上面一栏ShallowHeap和Retain Heap:
ShallowHeap: 对象本身占用内存的大小,不包含其引用的对象,没有多少参考价值;
RetainHeap: 包含所有引用或被引用对象占用的内存,比Shallow Heap更有参考价值。
2. 下面一栏Regex:
<Regex>:可以输入正则表达式去寻找你想要查看的class。
3. 再下来就是各种class条目,右键条目可以看到很多菜单选项,我这里列几条常用的:
Listobjects -> with incoming references:查看这个对象被哪些外部对象引用;
Listobjects -> with outcoming references:查看这个对象持有哪些外部对象引用;
PathTo GC Roots -> exclude all phantim/weak/soft etc.references:查看这个对象的GC Root,不包含虚、弱引用、软引用,剩下的就是强引用。从GC上说,除了强引用外,其他的引用在JVM需要的情况下是都可以 被GC掉的,如果一个对象始终无法被GC,就是因为强引用的存在,从而导致在GC的过程中一直得不到回收,因此就内存溢出了;
PathTo GC Roots -> exclude weak/soft references:查看这个对象的GCRoot,不含弱引用和软引用所有的引用;
MergeShortest path to GC root:找到从GC根节点到一个对象或一组对象的共同路径。
4. 条目最右侧SystemClass,Thread等字样,是指可能的GC root:
SystemClass:系统Class Loader加载的类. 例如java运行环境中rt.jar中类, 比如java.util.*package中的类;
JNILocal/ JNI Global:JNI 中的本地/全局变量, 用户自定义的JNI代码或是JVM内部的;
ThreadBlock:当前活动线程块引用的对象;
Thread:运行中的线程;
BusyMonitor:调用了wait(),notify(),或者是synchronized。例如,如果调用了synchronized(Object),或者进入了synchronized方法,那么静态方法指的类,非静态方法指的是对象;
JavaLocal:Java本地实例, 比如方法的入参和方法内创建的变量;
NativeStack:native代码里的传入参数或是返回值,比如file/net/IO方法以及反射的参数;
Finalizable:在一个队列里等待它的finalizer 运行的对象;
Unfinalized:一个有finalize方法的对象,还没有被finalize,同时也没有进入finalizer队列等待finalize;
Unreachable:引用到达不了的对象,在MAT里被标记为root用来retain object,否则是不会在分析中出现的;
JavaStack Frame:java栈帧包含了本地变量,当dump被解析时且在preferences里设置过把栈帧当做对象,这时才会产生;
Unknown:未知的Root类型。
这张图包含的信息非常多,我来带着大家一起解析一下。首先Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存,因此从上图中看,前两行的Retained Heap是最大的,我们分析内存泄漏时,内存最大的对象也是最应该去怀疑的。我们点击GroupResult by…按钮(红色横线),选择Package,这样我们就可以按照自己APP的包名去定位,如下图:
另外大家应该可以注意到,在每一行的最左边都有一个文件型的图标,这些图标有的左下角带有一个红色的点,有的则没有。带有红点的对象就表示是可以被GC Roots访问到的,根据上面的讲解,可以被GC Root访问到的对象都是无法被回收的。那么这就说明所有带红色的对象都是泄漏的对象吗?当然不是,因为有些对象系统需要一直使用,本来就不应该被回收。我们可以注意到,上图当中所有带红点的对象最右边都有写一个System Class,说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象,无需查看。
那么上图中就无法看出内存泄漏的原因了吗?确实,内存泄漏本来就不是这么容易找出的,我们还需要进一步进行分析。上图当中,除了带有SystemClass的行之外,最大的就是第一行的CalculatorText对象了,虽然CalculatorText对象现在不能被GC Roots访问到,但不代表着CalculatorText所持有的其它引用也不会被GCRoots访问到。现在我们可以对着第一行点击右键->Path To GC Roots -> exclude allphantim/weak/soft etc.references,我们这里直接把强引用之外的全部排除掉,结果如下图所示:
这里可以清楚的看到,LeakClass持有了CalculatorText对象,Root类型是Thread,就是线程持有对象,导致对象无法回收,造成了泄露。
通过这种方式,我们成功的找到了内存泄露的原因。这是DominatorTree中比较常用的一种分析方式,即搜索大内存对象通向GCRoots的路径,因为内存占用越高的对象越值得怀疑。
接下来我们再来学习一下Histogram的用法,回到Overview界面,点击Histogram,结果如下图所示:
图中,我们可以看到类实例对象的个数和大小。那么我们现在就通过Histogram又怎么去分析内存泄漏的原因。我们怀疑主Activity存在泄露,那么直接搜索Calculator,搜索结果如下图:
发现Calculator数目是13个,这太不正常,通常情况下一个Activity只有一个实例才对。接下来对Calculator右键,点开List objects -> with incomingreferences,查看一下到底是哪些外部对象持有了Calculator实例,结果如下图:
我们只需要查看强引用的部分,于是右键一条,选择Path To GC Roots -> exclude allphantim/weak/soft etc.references,结果如下图:
这里可以清楚的看到,也是LeakClass的Thread持有对象造成的内存泄漏,和之前分析的结果是一样的,内存泄露的原因同样找到了。
我再介绍几个MAT比较有用的功能菜单,这里我介绍三个,Group菜单,Export菜单,Compare菜单,其他功能可自行研究,菜单位置如下图:
1. Group Result by…菜单
回到Overview界面,点击Histogram,点Group Result by…菜单,有Group by Class,Group by superclass, Group by class loader,Group by package,举个例子,选择Group by package(按包名列组),如下图:
点开自己的包名,就可以清楚的看到自己的类里面的对象个数和占用内存大小了,更快的分析自己的应用各个类内存使用情况。
2. Export菜单
刚才我们得到了内存泄露的原因所在,如果我们想导出来,就可以按这个按钮,到处类型有Html,Text和CSV类型。
3. Compare to another heap Dump菜单
要使用这个菜单功能,必须Open 两个以上.hprof文件。我举个例子,我再打开一个com.android.calculator2_2.hprof文件,然后同样Groupby package,结果如下:
这是在没有操作apk的情况下得到的.hprof文件,Calculator(Activity)实例只有一个。
点击Compare菜单,选择com.android.calculator2_2.hprof,结果如下图:
可以清楚的看到,Calculator对象相对于com.android.calculator2_2.hprof多了12个,这显然是不正常的(其他的类的实例也是多了很多)。0表示两个文件中对应类的实例对象个数相同。
参考文章:
http://blog.csdn.net/guolin_blog/article/details/42238633
https://www.jianshu.com/p/5acb7146371b
http://ju.outofmemory.cn/entry/172684
http://ju.outofmemory.cn/entry/129445
最后
以上就是任性大碗为你收集整理的MAT工具使用使用MAT工具检测内存泄露问题的全部内容,希望文章能够帮你解决MAT工具使用使用MAT工具检测内存泄露问题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复