在这个系列的前四篇文章中,我分别介绍了 DVM、ART、内存泄漏和内存检测工具的相关知识点,这一篇我们通过一个小例子,来学习如何使用内存分析工具 MAT。
在进行内存分析时,我们可以使用 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。
我们需要准备一段发生内存泄漏代码,如下所示。
- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- LeakThread leakThread = new LeakThread();
- leakThread.start();
- }
- class LeakThread extends Thread {
- @Override
- public void run() {
- try {
- Thread.sleep(60 * 60 * 1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
上面的代码是很典型的内存泄漏的例子,原因就是非静态内部类 LeakThread 持有外部类 MainActivity 的引用,LeakThread 中做了耗时操作,导致 MainActivity 无法被释放,关于内存泄漏可以查看 Android 内存优化(三)避免可控的内存泄漏这篇文章。
生成 hpof 文件主要分为以下几个步骤:
1. 在 Android Studio 中打开 DDMS,运行程序。
2. 在 Devices 中选择要分析的应用程序进程,点击 Update Heap 按钮 (装有一半绿色液体的圆柱体)开始进行追踪。
3. 进行可能发生内存问题的操作(本文的例子就是不断的切换横竖屏)。
4. 点击 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 文件的保存路径。
除了用 DDMS 来生成 hpof 文件,还可以用 AS 的 Memory Monitor 来生成 hpof 文件。
生成 hpof 文件主要分为一下几个步骤:
1. 在 Android Monitor 中选择要分析的应用程序进程。
2. 进行可能发生内存问题的操作(本文的例子就是不断的切换横竖屏)。
3. 点击 Dump Java Heap 按钮,生成 hprof 文件,如下图所示。
Memory Monitor 生成的 hpof 文件也不是标准的,AS 提供了便捷的转换方式:Memory Monitor 生成的 hpof 文件都会显示在 AS 左侧的 Captures 标签中,在 Captures 标签中选择要转换的 hpof 文件,并点击鼠标右键,在弹出的菜单中选择 Export to standard.hprof 选项,即可导出标准的 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,下面分别对它们进行介绍。
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 一项单击鼠标右键,选择 Merge Shortest Paths to GC Root,如下图所示。
Merge Shortest Paths to GC Root 选项主要用来显示距离 GC Root 最短的路径,根据引用类型会有多种选项,比如 with all references 就是包含所有的引用,这里我们选择 exclude all phantom/weak/soft etc. references,因为这个选项排除了虚引用、弱引用和软引用,这些引用一般是可以被回收的。这时 MAT 就会给出 MainActivity 的 GC 引用链。
引用 MainActivity 的是 LeakThread,this$0 的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是 MainActivity,这将会导致 MainActivity 无法被 GC。
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 Root,并在选项中选择 exclude all phantom/weak/soft etc. references 如下图所示。
得出的结果和 3.1 节是相同的,引用 MainActivity 的是 LeakThread,这导致了 MainActivity 无法被 GC。
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 语句有很多用法,具体可以查看官方文档。
因为我们这个例子很简单,可以通过上面的方法来找到内存泄漏的原因,但是复杂的情况就需要通过对比 hpof 文件来进行分析了。使用步骤为:
1. 操作应用,生成第一个 hpof 文件。
2. 进行一段时间操作,再生成第二个 hpof 文件。
3. 用 MAT 打开这两个 hpof 文件。
4. 将第一个和第二个 hpof 文件的 Dominator Tree 或者 Histogram 添加到 Compare Basket 中,如下图所示。
5. 在 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
来源: http://blog.csdn.net/itachi85/article/details/77075455