本文首发于微信公众号「后厂技术官」
前言
在这个系列的前四篇文章中,我分别介绍了DVM、ART、内存泄漏和内存检测工具的相关知识点,这一篇我们通过一个小例子,来学习如何使用内存分析工具MAT。
1.概述
在进行内存分析时,我们可以使用Memory Monitor和Heap Dump来观察内存的使用情况、使用Allocation Tracker来跟踪内存分配的情况,也可以通过这些工具来找到疑似发生内存泄漏的位置。但是如果想要深入的进行分析并确定内存泄漏,就要分析
疑似发生内存泄漏时所生成堆存储文件。堆存储文件可以使用DDMS或者Memory Monitor来生成,输出的文件格式为hpof,而MAT就是来分析堆存储文件的。
MAT,全称为Memory Analysis Tool,是对内存进行详细分析的工具,它是Eclipse的插件,如果用Android Studio进行开发则需要单独下载它,下载地址为:http://eclipse.org/mat/,这篇文章MAT的版本为1.6.1。
2.生成hpof文件
2.1 准备内存泄漏代码
我们需要准备一段发生内存泄漏代码,如下所示。
public class MainActivity extends AppCompatActivity { |
上面的代码是很典型的内存泄漏的例子,原因就是非静态内部类LeakThread持有外部类MainActivity的引用,LeakThread中做了耗时操作,导致MainActivity无法被释放,关于内存泄漏可以查看Android内存优化(三)避免可控的内存泄漏这篇文章。
2.2 DDMS生成hpof文件
生成hpof文件主要分为以下几个步骤:
- 在Android Studio中打开DDMS,运行程序。
- 在Devices中选择要分析的应用程序进程,点击Update Heap按钮(装有一半绿色液体的圆柱体)开始进行追踪。
- 进行可能发生内存问题的操作(本文的例子就是不断的切换横竖屏)。
- 点击Dump HPROP File按钮结束追踪,生成并保存hprof文件,如下图所示。
DDMS生成的hprof文件并不是标准的,还需要将它转换为标准的hprof文件,这样才会被MAT识别从而进行分析,可以使用SDK自带的hprof-conv进行转换,它的路径在sdk/platform-tools中,进入到该路径执行以下语句即可:
hprof-conv D:\before.hprof D:\after.hprof |
其中 D:\before.hprof 是要转换的hprof文件路径,D:\after.hprof 则是转换后hprof文件的保存路径。
2.3 Memory Monitor生成hpof文件
除了用DDMS来生成hpof文件,还可以用AS的Memory Monitor来生成hpof文件。
生成hpof文件主要分为一下几个步骤:
- 在Android Monitor中选择要分析的应用程序进程。
- 进行可能发生内存问题的操作(本文的例子就是不断的切换横竖屏)。
- 点击Dump Java Heap按钮,生成hprof文件,如下图所示。
Memory Monitor生成的hpof文件也不是标准的,AS提供了便捷的转换方式:Memory Monitor生成的hpof文件都会显示在AS左侧的Captures标签中,在Captures标签中选择要转换的hpof文件,并点击鼠标右键,在弹出的菜单中选择Export to standard.hprof选项,即可导出标准的hpof文件,如下图所示。
3.MAT分析hpof文件
用MAT打开标准的hpof文件,选择Leak Suspects Report选项。这时MAT就会生成报告,这个报告分为两个标签页,一个是Overview,一个是Leak Suspects(内存泄漏猜想),如下图所示。
Leak Suspects中会给出了MAT认为可能出现内存泄漏问题的地方,上图共给出了3个内存泄漏猜想,通过点击每个内存泄漏猜想的Details可以看到更深入的分析清理情况。如果内存泄漏不是特别的明显,通过Leak Suspects是很难发现内存泄漏的位置。
打开Overview标签页,首先看到的是一个饼状图,它主要用来显示内存的消耗,饼状图的彩色区域代表被分配的内存,灰色区域的则是空闲内存,点击每个彩色区域可以看到这块区域的详细信息,如下图所示。
再往下看,Actions一栏的下面列出了MAT提供的四种Action,其中分析内存泄漏最常用的就是Histogram和Dominator Tree。我们点击Actions中给出的链接或者在MAT工具栏中就可以打开Dorminator Tree和Histogram,如下图所示。
其中左边第二个选项是Histogram,第三个选项是Dorminator Tree,第四个是OQL,下面分别对它们进行介绍。
3.1 Dominator Tree
Dorminator Tree意味支配树,从名称就可以看出Dorminator Tree更善于去分析对象的引用关系。
图中可以看出Dorminator Tree有三列数据。
- Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。如果是数组类型的对象,它的大小是数组元素的类型和数组长度决定。如果是非数组类型的对象,它的大小由其成员变量的数量和类型决定。
- Retained Heap:一个对象的Retained Set所包含对象所占内存的总大小。换句话说,Retained Heap就是当前对象被GC后,从Heap上总共能释放掉的内存。
Retained Set指的是这个对象本身和他持有引用的对象以及这些引用对象的Retained Set所占内存大小的总和,官方的图解如下所示。
从图中可以看出E的Retained Set为E和G。C的Retained Set为C、D、E、F、G、H。
MAT所定义的支配树就是从上图的引用树演化而来。在引用树当中,一条到Y的路径必然会经过X,这就是X支配Y。X直接支配Y则指的是在所有支配Y的对象中,X是Y最近的一个对象。支配树就是反映的这种直接支配关系,在支配树中,父节点直接支配子节点。下图就是官方提供的一个从引用树到支配树的转换示意图。
C直接支配D、E,因此C是D、E的父节点,这一点根据上面的阐述很容易得出结论。C直接支配H,这可能会有些疑问,能到达H的主要有两条路径,而这两条路径FD和GE都不是必须要经过的节点,只有C满足了这一点,因此C直接支配H,C就是H的父节点。通过支配树,我们就可以很容易的分析一个对象的Retained Set,比如E被回收,则会释放E、G的内存,而不会释放H的内存,因为F可能还引用着H,只有C被回收,H的内存才会被释放。
这里对支配树进行了讲解,我们可以得出一个结论:通过MAT提供的Dominator Tree,可以很清晰的得到一个对象的直接支配对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄漏。
在Dominator Tree的顶部Regex可以输入过滤条件(支持正则表达式),如果是查找Activity内存泄漏,可以在Regex中输入Activity的名称,比如我们这个例子可以输入MainActivity,效果如下图所示。
Dominator Tree中列出了很多MainActivity实例,MainActivity是不该有这么多实例的,基本可以断定发生了内存泄漏,具体内存泄漏的原因,可以查看GC引用链。在MainActivity一项单击鼠标右键,选择Path To GC Roots,如下图所示。
Path To GC Roots选项用来表示从对象到GC Roots的路径,根据引用类型会有多种选项,比如with all references就是包含所有的引用,这里我们选择exclude all phantom/weak/soft etc. references,因为这个选项排除了虚引用、弱引用和软引用,这些引用一般是可以被回收的。这时MAT就会给出MainActivity的GC引用链。
引用MainActivity的是LeakThread,this$0的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是MainActivity,这将会导致MainActivity无法被GC。
3.2 Histogram
Histogram与Dominator Tree不同的是,Dominator Tree是在对象实例的角度上进行分析,注重引用关系分析,而Histogram则在类的角度上进行分析,注重量的分析。
Histogram中的内容如下图所示。
可以看到Histogram中共用四列数据,关于Shallow Heap和Shallow Heap的含义我们在3.1节已经知道了,剩余的 Class Name代表类名,Objects代表对象实例的个数。
在Histogram的顶部Regex同样可以输入过滤条件,这里同样输入MainActivity,效果如下图所示。
MainActivity和LeakThread实例各为11个,基本上可以断定发生了内存泄漏。具体内存泄漏的原因,同样可以查看GC引用链。在MainActivity一项单击鼠标右键,选择Merge Shortest Paths to GC roots ,并在选项中选择exclude all phantom/weak/soft etc. references如下图所示。
Histogram是在类的角度进行分析,而Path To GC Roots是用来分析单个对象的,因此在Histogram无法使用Path To GC Roots查询,可以使用Merge Shortest Paths to GC roots查询,它表示从GC roots到一个或一组对象的公共路径。
得出的结果和3.1节是相同的,引用MainActivity的是LeakThread,这导致了MainActivity无法被GC。
3.3 OQL
OQL全称为Object Query Language,类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有的对象。它的查询语句的基本格式为:
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression>] |
当我们输入select * from instanceof android.app.Activity并按下F5时(或者按下工具栏的红色叹号),会将当前内存中所有Activity都显示出来,如下图所示。
如果Activty比较多,或者你想查找具体的类,可以直接输入具体类的完整名称:
select * from com.example.liuwangshu.leak.MainActivity |
通过查看GC引用链也可以找到内存泄漏的原因。关于OQL语句有很多用法,具体可以查看官方文档。
3.4 对比hpof文件
因为我们这个例子很简单,可以通过上面的方法来找到内存泄漏的原因,但是复杂的情况就需要通过对比hpof文件来进行分析了。使用步骤为:
- 操作应用,生成第一个hpof文件。
- 进行一段时间操作,再生成第二个hpof文件。
- 用MAT打开这两个hpof文件。
- 将第一个和第二个hpof文件的Dominator Tree或者Histogram添加到Compare Basket中,如下图所示。
- 在Compare Basket中点击红色叹号按钮生成Compared Tables,Compared Tables如下图所示。
在Compared Tables也有顶部Regex,输入MainActivity进行筛选。
可以看到MainActivity在这一过程中增加了6个,MainActivity的实例不应该增加的,这说明发生了内存泄漏,可以通过查看GC引用链来找到内存泄漏的具体的原因。
除了上面的对比方法,Histogram还可以通过工具栏的对比按钮来进行对比:
生成的结果和Compared Tables类似,我们输入MainActivity进行筛选:
可以看到第二个hpof文件比第一个hpof文件多了6个MainActivity实例。
MAT还有很多功能,这里也只介绍了常用的功能,其他的功能就需要读者在使用过程中去发现并积累。
参考资料
《Android群英传 神兵利器》
《Android应用性能优化最佳实践》
《高性能Android应用开发》
利用MAT进行内存泄露分析
Android最佳性能实践(二)——分析内存的使用情况
Memory Analyzer