android 如何分析应用的内存(十七)——使用MAT查看Android堆
前一篇文章,介绍了使用Android profiler中的memory profiler来查看Android的堆情况。
如Android 堆中有哪些对象,这些对象的引用情况是什么样子的。
可是我们依然面临一个比较严峻的挑战:不管是app开发者,还是内存分析者而言,堆中的对象,非常之多,不仅有Android 原生的类,还有第三方库使用的类。这些类在使用过程中,也可能因为有较大的shallow size 或者retained size而混淆内存的分析。
为了解决这样的问题,我们更希望,通过不同时间点的堆,进行差分比较。即在时刻t1生成的堆heap1和t2时刻生成的堆heap2进行相互比较。
为此,我们将介绍java开发中重要的内存分析工具MAT。
MAT使用前的准备
在上一篇文章中,我们使用AS捕获了堆,现在我们需要将其导出,用在MAT工具上。如下图:
接下来将Android保存的heap dump进行格式转换,以满足MAT的需求。转换格式的工具在Android SDK中。如下:
/Users/biaowan/Library/Android/sdk/platform-tools/hprof-conv ./mat/memory-test_malloc_int\[\].hprof mat_test1.hprof
MAT的使用
打开MAT之后,进入菜单栏->File->Open File。然后选择刚才转换之后的mat_test.hprof文件。如下图
可见,主界面显示一个overview的界面,该界面用英文详细表述了具体细节,不在赘述。下面解释上图的八个标记。
-
标记1:打开overview 界面。即上面的主界面。
-
标记2:打开当前堆中的对象分布情况,默认按照类名进行排序。右键可以进行相应操作,各个操作什么意思,已经标明。如下图
-
标记3:显示本heap中的所有dominator tree(注意:dominator tree,已经在上一篇文章中介绍:android 如何分析应用的内存(十六)——使用AS查看Android堆:http://t.csdn.cn/GTWpR). 而各个对象应该怎么查看其对应的dominator tree。见标记2对应的右键说明。
-
标记4:Open Object Qurey Language,类似于使用SQL语句进行查询。因为MAT提供的菜单功能已经完全够Android使用,因此本文不再介绍
-
标记5:展示线程的名字,堆栈,本地变量等。但是Android 没有提供这个功能,因此无法使用
-
标记6:打印各种报告,如下图标记
-
标记7:即为标记2中,右键支持的各种操作,详见标记2
-
标记8:搜索按钮,可以按照地址进行搜索
为了能够详细说明,如何操作MAT,下文将会以各种问题作为模板,详细介绍操作过程
问题1:如何查看某个类有哪些对象
- 点击标记1,打开所有类的列表。
- 在第一行,键入需要查找的类,或者按照不同的大小进行排序和过滤
- 选中类,然后点击右键,选择List Objects.然后按照需求列出各个对象
问题2:如何查看某个对象到GC root的引用链
- 选中某个对象,右键选中Paths to GC root
- 再次选中exclude all xxx references
可见整个引用链清晰明了,不在绘图说明
问题3:如何查看对象的Dominator tree(支配树)
- 选中对象,右键选择Java Basics
- 再次选中Open in Dominator tree
- 在弹出的框中,选择finish。默认以对象排序
从图中可以看到TaskRunable对象直接支配两个对象,一个int数组,一个弱引用
问题4:如何查看一个对象的直接支配者
- 选中对象,然后右键,选择immediate dominator
- 在弹出框中,选择finish
可以看到我们选中的TaskRunable对象的直接支配者是一个Task对象
问题5:如何查看类加载器,是否重复加载同一个类
- 点击标记1,打开overview界面
- 滚动界面到最底下
- 选择Duplicate classes
问题6:如何查看堆中,最占内存的部分
- 点击标记1,打开overview界面
- 滚动至最底下
- 选择Top cosumer
从图中可以看到,分别按照对象,类,类加载器,包名列出了最占内存的部分
问题7:如何查看堆的报告
- 点击标记6.选择Heap dump overview
- 在对报告中,点击table of content 查看内容表(该字段,在报告的底部)
从中也可以直接查看最占内存的对象
问题8:如何进行泄露检查
- 点击标记6,选择Leak Suspect
从图中可以看到,有三个怀疑的对象,往下滚动,可以看到三个怀疑对象的详细细节,如下
图中,简要说明了Task类,有2100个实例,占了29.51%的内容。点击details它会显示相应的引用链路径。可清晰看到GC root的整个引用链。
多个Heap进行差分分析,查找内存问题
为了一步步演练,如何使用多个heap进行差分分析,我们选择上一篇文章方案2中的例子:android 如何分析应用的内存(十六)——使用AS查看Android堆:http://t.csdn.cn/JYGFC。然后在同一个进程的两个不同时刻,分别选取不同的heap,分别叫as_heap1.hprof和as_heap2.hprof.
场景1:MAT 自动分析两个堆之间的内存泄露
-
按照上文提及的hprof-conv工具,将as_heap1.hprof,as_heap2.hprof分别转换为mat_heap1.hprof,mat_heap2.hprof。然后用mat工具将其打开。
-
打开第二个heap的overvie操作栏,即标记1。滚动到最底部
-
选择Leak suspects by Snapshot comparision.
-
在弹出的框中,选择mat_heap1.hprof。然后点击finish。让mat_heap2.hprof与mat_heap1.hprof做差分分析,然后给出一个报告,如下(需要等待一段时间)
从图中可以看到,怀疑com.example.test_malloc.Task对象泄露,它有4900个对象,占整个堆的49.26%。点击details可以看到这个引用链,如下图
从图中可以看到,Task被DeviceManager的listener所持有,导致GC无法回收。所以找到了内存泄露点。
场景2:无法自动分析时,手动分析
当两个heap堆,间隔时间较短,泄露的对象,占据整个堆的空间较小,此时mat无法进行自动分析。此时我们可以手动分析。
接下来,我们用时间间隔较小的两个堆,分别叫mat_heap3.hprof和mat_heap4.hprof
注意:mat_heap3.hprof和mat_heap4.hprof是用AS 重新抓取的时间间隔较近的两个堆
-
用mat打开mat_heap3.hprof,和mat_heap4.hprof
-
按照问题6,输出消耗内存最大的部分。下面是mat_heap3.hprof的报告。
从中,我们看到,占据内存最大的首要对象是int数组。接下来我们手动分析两个堆中的int数组之间的差距——即mat_heap4.hprof比mat_heap3.hprof多了哪几个int数组
-
点击mat_heap3.hprof的统计信息,即标记2.然后选中int[]。右键列出所有的对象。如下图
-
点击操作历史记录栏,右键list_objects… 然后点击add to compare basket。如下图
-
因为我们需要比较两个heap堆的int[]情况,因此选中mat_heap4.hprof之后,按照步骤3,4做同样操作。将会在compare basket窗口两个需要比较的对象。然后点击感叹号,开始比较即可。如下:
对测试结果,进行简单排序,shallow_heap #1 升序排列。即可展示heap3中没有而heap4中具有的对象。这也是从抓取heap3时刻,到抓取heap4时刻之间,堆中多出来的int数组对象。为前排的10个对象。
按照前文的问题2即可查看其引用链,从而分析被谁持有,为何没有被释放掉。
在第2步中,输出的top comsumer除了int数组以外,还有其他的对象,因此按照步骤3,4,5即可进行两个堆的比较。我们已经以int[]为例子,做了详细说明,就不再一一比较。
除了使用top comsumer辅助定位需要比较的对象以外,还可以对任何怀疑的对象进行比较。步骤完全相同。
至此,MAT的使用介绍完毕。
MAT弥补了AS在内存分析上的如下不足:
- 无法自定义Retained Set(这对于大型应用很有用)
- 无法进行地址查找
- 无法进行堆之间的比较
- 无法按照需要进行排序
- 无法按照需要进行过滤等
虽然MAT已经足够强大,但是依然还有一个内存问题,悬而未决——怎么才能知道这些内存泄露是由哪一个线程触发,它们又有怎样的调用栈?
在多线程编程中,对象的泄露,即可能是对象之间的引用不合理,也可能是线程之间的逻辑不合理,如生产线程和消费线程不够合理等等。MAT无法解决Android线程所带来的内存泄露。
接下里,请期待如何用工具找到这种多线程带来的内存泄露。