Android内存泄露分析利器——MAT

简介

讲到Android的内存泄露,就不能不提及内存泄露分析的利器——MAT。
MAT全称Eclipse Memory Analyzer,分为Eclipse插件版和独立版两个版本。
下载地址在MAT
如果平时使用Eclipse开发,那么插件版MAT会非常方便,插件安装的update地址也在上述下载地址里面有。
如果平时使用Android Studio开发,那么就只能使用独立版的MAT了。

内存泄露分析

内存泄露代码

这里,我们来模拟一种Activity内存泄漏的场景,就是之前提过的内部类内存泄露问题。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

LeakClass leakClass = new LeakClass();
leakClass.start();
}

class LeakClass extends Thread {

@Override
public void run() {
while (true) {
try {
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

具体泄露原理不说了,运行程序,然后不断地旋转手机让程序在横屏和竖屏之间切换。
因为每切换一次Activity都会经历一个重新创建的过程,而前面创建的Activity又无法得到回收,那么长时间操作下我们的应用程序所占用的内存就会越来越高,最终出现OutOfMemoryError。
接下来就要通过MAT去具体分析内存泄露问题了。

Dump Hprof File

我们要进行内存泄露分析,最基本的就是获取当前的Heap情况。
在Android中,一个heap dump就是一个当前程序的Heap快照,可以获知程序当前的Heap内存使用情况。
它保存为一种叫做HPROF的二进制格式。
下面介绍Dump Heap的过程。

Eclipse Dump Heap

eclipse_dump_heap

Android Studio Dump Heap

android_studio_dump_heap

转换Hprof文件

默认Dump出来的Hprof文件是Dalvik格式,我们要将其转换成J2SE格式的才能进行内存泄露分析。
如果Eclipse装了MAT插件,那么Dump Heap后,转换的过程会自动进行,稍等片刻,就会直接自动在Eclipse中打开转换后的Hprof文件
如果Eclipse没有安装MAT插件,那么,点击Dump Hprof File后,会让你另存为一个本地文件。
存储后,我们就需要将这个Hprof文件进行转换了。
转换工作要利用hprof-conv命令,在控制台中输入:

1
hprof-conv dump.hprof converted-dump.hprof

hprof-conv命令文件存放于/platform-tools目录下面。

如果使用的是Android Studio,可以右键生成的文件进行转换:
android_studio_convert_hprof

具体分析

在MAT中打开我们转换之后的hprof文件,弹出的Get Start界面就选默认的Leak Suspects Report,点击Finish按钮。
get_start
之后就是MAT的基础界面了:
mat_base
MAT提供了非常强大而细致的功能,但常用的就那么几个。
上图中的饼图展示了最大的几个对象所占内存的比例,但大部分情况下,显示的都是系统变量,没什么意义。
一般常用的功能是下面几个:

  • Leak Suspects:列出疑似内存泄露问题。
  • Histogram:可以列出内存中每个对象的名字、数量以及大小。
  • Dominator Tree:会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。

Leak Suspects

系统会在弹出首页的同时列出其认为疑似内存泄露的问题:
leak_suspects
其不止列出问题描述,还有对象的具体信息,以及可以点击Detail看详细问题分析。
但是工具的怀疑未必真实存在,很多情况下,提出的问题涉及的对象都是系统对象,意义不大。

Dominator Tree

点击Dominator Tree,结果展示如下:
dominator_tree
Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存,下文有具体阐述。
具体分析的时候,应该从Retained Heap最大的对象开始看起。
在每一行的最左边都有一个文件型的图标,其中一些图标的左下角带有一个橙色的点,另外的则没有。这个是什么呢?
带有橙色的对象就表示可以被GC Roots访问到的,根据上面的讲解,可以被GC Root访问到的对象都是无法被回收的。
需要注意的是,无法被回收不代表就是泄露对象,因为很多都是系统需要使用的对象。
我们可以注意到,大部分橙色对象最右边都有写一个System Class,说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象。

在Dominator Tree中寻找我们的内存泄露对象,无异于大海捞针,能找到,纯粹是内存泄露对象太大太明显了,或者祖坟冒青烟。
大部分情况,我们对自己的泄露对象有一个初步的猜测,就可以在Dominator Tree里面去寻找了。
在最顶端输入我们觉得可能内存泄露的对象,比如我们怀疑我们的MainActivity有泄漏,就可以这样:
dominator_tree_search
可以看到我们这里有好多好多的MainActivity对象(原谅我闲的蛋疼,旋转屏幕无数次)。
理论上,我们的Activity在旋转屏幕的时候会顺利被销毁,但是我们这把却留存了那么多的MainActivity对象,说明发生了内存泄露。
选择其中一个对象,右击,选择Path to GCRoot->exclude weak/soft reference。
dominator_tree_search_gc_path
为什么选择exclude weak/soft references呢?因为弱引用是不会阻止对象被垃圾回收器回收的,所以我们这里直接把它排除掉。
结果如下:
dominator_tree_search_gc_path_result
可以看到,我们的MainActivity$LeakClass对象,左下角有橙色图标,说明能被GC Root访问,并且是我们自己的Thread,不是System Class。
所以,这就是我们的内存泄露根源,MainActivity$LeakClass不能被回收,那么其引用的MainActivity也就不能回收了。

Histogram

Histogram也可以去分析大内存对象,但是意义不大。
Histogram更重要的功能是可以显示对象的数目,同样,我们在Histogram中搜索MainActivity,结果如下:
histogram_search
可以看到,我们内存里面有超多的MainActivity对象,说明我们泄露了。
对着MainActivity右键 -> List objects -> with incoming references查看具体MainActivity实例:

  • outgoing references :表示该对象的出节点(被该对象引用的对象)。
  • incoming references :表示该对象的入节点(引用到该对象的对象)。

histogram_list_object
结果如下:
histogram_list_object_result
接下来,像上面一样,选择其中一个对象,右击,选择Path to GCRoot->exclude weak/soft reference,就能进行我们的内存泄露分析了。

几个重要的概念

Shallow heap

Shallow size就是对象本身占用内存的大小,不包含其引用的对象。

  • 常规对象(非数组)的Shallow size有其成员变量的数量和类型决定。
  • 数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定
Retained Heap

Retained Heap表示如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。
相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。
retained_objects
retained_objects_2
从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。
但是因为可以通过GC Roots访问,所以上图的obj3不是蓝色节点。
而在下图obj3却是蓝色,因为它已经被包含在retained集合内。
所以对于上图,obj1的retained size是obj1、obj2、obj4的shallow size总和。
下图的retained size是obj1、obj2、obj3、obj4的shallow size总和。
详见Shallow and retained sizes

Path to GC Root
  • Strong Ref(强引用):通常我们编写的代码都是Strong Ref,于此对应的是强可达性,只有去掉强可达,对象才被回收。
  • Soft Ref(软引用):对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。
  • Weak Ref(弱引用):比Soft Ref更弱,当发现不存在Strong Ref时,立刻回收对象而不必等到内存吃紧的时候。通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。
  • Phantom Ref(虚引用):根本不会在内存中保持任何对象,你只能使用Phantom Ref本身。一般用于在进入finalize()方法后进行特殊的清理过程,通过 java.lang.ref.PhantomReference实现。

总结

需要注意的是,MAT并不会准确地告诉我们哪里发生了内存泄漏,而是会提供一大堆的数据和线索,我们需要自己去分析这些数据来去判断到底是不是真的发生了内存泄漏
如果想准确的知道哪里发生了内存泄露,市面上有一款优秀的工具LeakCanary,以后有机会讲一下。
工具是死的,人是活的,参照前面的文章,尽量避免内存泄露才是王道。