对于Android这种手持设备来说,通常不会带有太大的内存,而且一般用户都是长时间不重启手机,所以编写程序的时候必须要非常小心的使用内存,尽量避免有内存泄露的问题出现。通常分析程序中潜在内存泄露的问题是一件很有难度的工作,一般都是由团队中的资深工程师负责,而且随着程序代码量的提高,难度还会逐步加大。

今天要介绍一个在Eclipse中使用的内存分析工具——MAT(Eclipse Memory Analyzer,主页在http://www.eclipse.org/mat/)。它是一个功能非常丰富的Java堆转储文件分析工具,而且简单易用,只需点几下下鼠标就可以生成一个专业的分析报告。更重要的是,它和DDMS能够无缝集成,可以帮助你发现Android程序中潜在的内存泄露问题,同时可以协助你优化程序,更加高效的使用内存。

一、安装 MAT

MAT是Eclipse中的一个插件,所以安装非常简单。

首先,通过 Help -> Install Software... 启动软件安装页面:

选择Add…,会弹出一个对话框,名字(Name)中填MAT(其实随便填什么),位置(Location)中填http://download.eclipse.org/mat/1.4/update-site/,然后点OK:

接下来选择你想要安装的 MAT 的功能。因为只需要在Eclipse中使用MAT,所以这里只需要选择Memory Analyzer for Eclipse IDE。特别提一下,Memory Analyzer (Chart) 这个功能是一个可选的安装项,主要用来将数据生成相关的可视化报表,通常这样更加直观,更容易发现问题。

剩下的就一路点Next就好了。

二、几个基本概念

要想看懂MAT生成的报告,还需要掌握几个基本的概念。

Java内存回收

Java程序中,内存空间中垃圾回收的工作是由垃圾回收器(Garbage Collector,GC)完成的。它的核心思想是,对虚拟机中堆内存空间中的对象进行识别,如果对象正在被别的对象引用,那么称其为存活对象;反之,如果对象不再被任何别的对象所引用,则为垃圾对象,可以回收其占据的空间,用于再分配。

对于Android程序来说,其Dalvik虚拟机本身就是一个类Java的虚拟机实现,也具有主动垃圾回收功能。

GC根对象(GC Root)

在垃圾回收机制中,有一组特殊的对象被称为根对象,它们是一组被虚拟机直接引用的对象。比如,正在运行的线程对象,系统调用栈里面的对象,以及被系统类加载器所加载进来的对象。堆空间中的每个对象都是由一个根对象为起点被层层引用的,只有它们引用别的对象,没有其它任何对象可以引用它们。因此,如果一个对象还被某一个存活的根元素所直接或间接的引用,就会被认为是存活对象,不能被回收,进行内存释放。

Shallow Size和Retained Size

1)Shallow Size(表面大小)是指对象本身所占用的内存空间大小,不包含被其引用到的对象所占的内存空间。一个对象的Shallow Size取决于这个对象的实例变量的类型和个数。一个数组对象的Shallow Size是数组中保存的对象的Shallow Size乘以数组元素的个数。一个集合对象的Shallow Size是集合内所有对象的Shallow Size之和。

2)Retained Size(保留大小)是指对象本身的Shallow Size加上从其本身开始所能直接或间接访问到的,且只能由其开始才能访问到的所有对象的Shallow Size之和。这个大小就表明,如果将这个对象释放之后,垃圾回收器所能回收的所有内存大小。

下面举个例子说明,假设对象的引用关系如下:

第一张图中计算对象obj1的Retained Size要包含obj1、obj2和obj4,因为obj3和obj5还可以被根对象引用到,所以不能被包含进来。而在第二张图中,obj3只能被obj1间接引用到,所以要包含进来。

支配树(Dominator Tree)

支配树的定义是,如果在对象引用关系图中,从任意一个对象,如果想访问对象Y的话,必须通过对象X,那么对象X就处于对象Y的支配(Dominate)地位。

可以看出来,支配树的概念其实和前面的Retained Size是相关联的。某个对象的Retained Size就是从这个对象开始,以及从它开始所有支配树子节点的Shallow Size之和。

而且,所有根节点一定是支配树中的顶层节点,反之不一定。

堆转储文件(Heap Dump)

所谓堆转储文件,是在一个特定时间点,对Java进程的内存快照文件。它包含了快照被触发时,Java对象和类在堆中的使用情况。由于快照只是一瞬间的事情,所以只能包含在这个时间点各个对象之间的引用关系,而无法知道一个对象在什么时间点,由哪个方法创建等信息。

MAT其实就是一个通用的Java堆转储文件的分析工具,而通过Android自带的DDMS,可以获得指定设备上指定进程的堆转储文件,将两者结合起来刚好可以达到分析Android程序内存使用状况的目的。

三、如何分析

首先,将你想要分析的程序启动起来,并确保其是可调试的(即在AndroidManifest.xml文件中申明android:debuggable=”true”)。

然后,用DDMS收集Android设备上,你想调试程序的堆转储文件。这步也很简单,在DDMS中,选取你的程序,并点击Dump HPROF file:

DDMS生成转储文件可能要花一点时间,请耐心等待一会。当转储文件生成好了之后,Eclipse会自动调用MAT进行分析,并生成报告:

在这份报告中,我们可以获得如下信息:

1)Histogram:列出了当前程序中每个类被实例化出了多少个对象;

2)Dominator Tree:列出了当前程序中各个活动对象间的支配树关系;

3)TopConsumer:列出了当前程序中Retained Size最大的前几个对象等信息;

4)DuplicateClasses:列出了当前程序中是否有相同的类被不同的类加载器加载的情况。

Histogram和Dominator Tree的区别是分析问题的角度不一样,Histogram是站在类的角度上去分析,而Dominator Tree是站的对象实例的角度上去分析,Dominator Tree可以更方便的看出各个对象间的引用关系。

有了这些武器之后,分析内存泄露问题就变得得心应手了,一般的流程如下:

1)在Histogram和Dominator Tree视图中,根据Retained Heap排序,找出排名靠前的几个类和对象。这些都是后面要重点进行分析的对象。

2)每隔一定时间段生成一次MAT分析报告,对比这几次报告,在在Histogram和Dominator Tree视图中找出Retailed Heap不断增大的几个类和对象。持续占用内存,还不释放,通常意味着有潜在的内存泄露问题。

3)当然,如果一个对象的Retained Size很大,并不一定代表它一定是有问题的,还需要具体情况具体分析。具体来说,我们可以分析一个对象到根对象的引用路径来分析为什么该对象不能被顺利回收。如果说一个对象已经不被任何程序逻辑所需要,但是还存在被根对象引用的情况,基本上就可以说这里存在内存泄露的问题。通过Histogram或者Dominator Tree视图找到疑似有问题的类或者对象之后,选择Path to GC Roots或者Merge Shortest Paths to GC Roots。这里有很多过滤选项,一般来讲可以选择exclude all phantom/weak/soft etc.references。这样就排除了虚引用、弱引用、以及软引用,剩下的就是强引用。除了强引用外,其它任何类型的引用,在Java虚拟机需要的情况下,都可以被GC释放掉。如果一个对象始终无法被释放,肯定是因为有强引用存在。

4)接下来就需要直接定位具体的代码,看看如何释放这些不该存在的对象。找到原因,清理干净后,再对照之前的操作,看看对象是否任然持续增长。如果不再,则说明这个潜在的内存泄露问题已经被修复了。

posted on 2016-01-19 09:44  一个人的天空@  阅读(851)  评论(1编辑  收藏  举报