简介 php
移动平台上的开发和内存管理紧密相关。尽管随着科技的进步,现今移动设备上的内存大小已经达到了低端桌面设备的水平,可是现今开发的应用程序对内存的需求也在同步增加。主要问题出在设备的屏幕尺寸上-分辨率越高须要的内存越多。熟悉Android平台的开发人员通常都知道垃圾回收器并不能完全杜绝内存泄露问题,对于大型应用而言,内存泄露对性能产生的影响是难以估量的,所以开发人员必需要有内存分析的能力。本文介绍一些有用的工具,以及如何使用它们来检测这些关键的内存泄露问题。 html
有不少工具能够用来帮助检测内存泄露问题,这里列举了一些,并附上一点相应的介绍: android
工具名称: 正则表达式 |
简介: 编程 |
DDMS (Dalvik调试监视服务器) 数组 |
和Android一块儿推出的调试工具(android sdk的tools目录下就有)。提供端口转发服务、截屏、线程监控、堆dump、logcat、进程和无线状态监控以及一些其余功能。能够经过”./ddms”命令启动该工具,同时它也被集成在ADT中,安装之后Eclipse切到DDMS视图便可。 浏览器 |
MAT (内存分析工具) 服务器 |
快速的Java堆分析器,该工具能够检测到内存泄露,下降内存消耗,它有着很是强大的解析堆内存空间dump能力。还有不少其余功能没法在这里一一列出,能够安装一下MAT的eclipse插件试试,要活的更多详细信息请点击这里。 app |
内存分析涉及到不少专用术语,他们在本文中将频繁出现,这里给出每一个术语的定义: dom
术语: |
定义: |
堆大小 |
分配给Java堆的内存,在Android平台,这些内存都是针对每一个Activity分配的(这还取决于设备) |
堆转储文件 |
一个包含了堆中信息的二进制文件 |
支配树(Dominator Tree) |
一个用来图形化展现对象之间关系的工具,详情请参考wiki |
内存泄露 |
内存泄露是指有个引用指向一个再也不被使用的对象,致使该对象不会被垃圾回收器回收。若是这个对象有个引用指向一个包括不少其余对象的集合,就会致使这些对象都不会被垃圾回收。所以,须要牢记,垃圾回收没法杜绝内存泄露。 |
GC根 |
GC根是指那些假设可达的对象。 一般包括全部当前栈和系统的类加载器加载的类中引用的对象。(【译者注】栈里引用的对象是指当前执行的方法里的局部变量指向的对象,系统加载器加载的类引用的对象包括类的静态属性引用的对象) |
内存泄露是指有个引用指向一个再也不被使用的对象,致使该对象不会被垃圾回收器回收。若是这个对象有个引用指向一个包括不少其余对象的集合,就会致使这些对象都不会被垃圾回收。
图. 1
图. 2
GC后:回收对象A(300字节)从而致使回收对象B(50字节)和C(50字节),同时释放了对象B之后对象D也会被回收(100字节),所以回收对象A就能够释放500字节的内存,所谓保留队正式这些对象直接占用的浅堆总和。
图 3
检测内存泄露
Logcat输出的log
第一种发现内存泄露的方法是检查logcat输出的log。当垃圾回收器工做时,能够在Logcat中看到它的消息,这消息长的样子相似于:
D/dalvikm( 14302): GC_CONCURRENT freed 2349K, 65% free 3246K/9551K, external 4703K/5261K, paused 2ms+2ms
这条消息的第一个部分说明该消息产生的缘由,一共有四种类型:
GC_CONCURRENT |
当堆变得很大,防止出现堆溢出异常时产生 |
GC_FOR_MALLOC |
若是GC_CONCURENT类型的操做没有及时运行,而且应用程序还须要分配更多内存时产生。 |
GC_EXTERNAL_ALLOC |
在Android3.0 (Honeycomb)之前,释放经过外部内存(externel memory, 经过JNI代码中malloc分配获得的内存)时产生。Android3.0和更高版本中再也不有这种类型的内存分配了。 |
GC_EXPLICIT |
调用System.gc时产生 |
“freed 2349K,” – 说明释放了多少内存.
“65% free 3246K/9551K” – 65%表示目前可分配内存占比例,3426K表示当前活动对象所占内存,9551K表示堆大小。
“external 4703K/5261K” – indicates external memory allocation, how much external memory the app has allocated and the soft limit of allocation.说明外部内存的分配,已经分配了多少以及可以分配的上限。
“paused 2ms+2ms” –说明GC运行完成须要的时间。
有了这些信息,咱们就能够知道GC运行几回之后有没有成功释放出一些内存,若是分配出去的内存在这几个周期中持续增长,那么很明显存在内存泄露。下面的例子中就是存在内存泄漏时的Log。(【译者注】图片有点不清楚,可是大概能够看出来GC运行了好屡次,可分配内存比例反而从47%降到45%了)
图. 4
OutOfMemoryError 异常
跟踪内存分配状况
成功的发现内存泄露问题之后,就应该查找根源在哪里了,有两个工具能够用来辅助分析问题根源所在。
DDMS
DDMS是一个强大的工具,他能够提供有很价值的信息,它还能够生成一个HPROF dump文件,该文件可使用MAT打开分析。在Eclipse中打开DDMS,只需安装ADT插件之后打开DDMS视图便可。
图. 5
图. 6
图. 7
内存分析工具 (MAT)
MAT是个强大的内存分析工具,能够单独使用也能够做为Eclipse的插件(【译者注】这个工具不在ADT中,能够在http://www.eclipse.org/mat/downloads.php下载,有stand-alone版本和Eclipse安装的update URL),这两种使用方法惟一的区别就是如何打开一个HPROF文件。独立版本须要一个打包好的HPROF文件。咱们可使用android adk自带的hprof-conv工具(在android sdk的tools目录下)打包。若是使用Eclipse插件版的MAT则不须要,直接在Eclipse中打开MAT视图便可。
概述
当打开HPROF文件后,能够看到一个Overview界面,它由如下元素构成:
图. 8
直方图(Histogram)
MAT最有用的工具之一,它能够列出任意一个类的实例数。查找内存泄露或者其余内存方面问题是,首先看看最有可能出问题的类,这个类有多少个实例是个比较好的选择。它支持使用正则表达式来查找某个特定的类,还能够计算出该类全部对象的保留堆最小值或者精确值。
图. 9
另外,当选择了某条显示条目后,能够经过右击弹出菜单。在诊断内存相关问题时,这个菜单是个很是重要的工具。若是开发者怀疑这里有个内存泄露,能够经过菜单直接查看该类的对象持有哪些其余对象,固然,MAT支持过滤查询结果(好比说限制被持有对象的类型)。查询结果出来时,列表经过另一个有用的工具-”Path toGC Roots”-展现给开发人员。它支持多种过滤选项,好比说排除弱引用-这是最多见的一个选项,由于当GC运行时,被弱引用持有的对象会被GC直接回收,因此这种对象是不会形成内存泄露的,通常直接把这种信息排除。若是MAT预约义的查询不能知足用户需求的话,它还支持本身定制查询,定制的自由度很是大,拥有无限的可能。本文稍后会介绍如何高效的定制查询。
图. 10
图 11
支配树(Dominator Tree)
支配树能够算是MAT中第二有用的工具,它能够将全部对象按照保留堆大小排序显示。用户能够直接在“Overview”选项页中点击“Dominator Tree”进入该工具,也能够在上面提到的菜单中选择“immediate dominators”进入该工具。前者显示dump文件中全部的对象,后者会从类的层面上查找并聚合全部支配关系。支配树有如下重要属性:
这三个属性对于理解支配树而言很是重要,一个熟练的开发人员能够经过这个工具快速的找出持有对象中哪些是不须要的以及每一个对象的保留堆。
图. 12
查询(Queries)
查询是用来检查对象树的基本工具,内存分析就是在许多对象中查找不但愿看到的引用关系的过程-这件事听上去容易作起来难。若是能够过滤这些对象和应用关系的话可使这项复杂的运动简单很多。一个开发人员想要成功的调试内存问题,必须掌握两个关键点。第一个是对本身的应用充分了解,若是对本身应用程序中的对象之间的关系不够了解的话,是不能找到内存问题的。第二个是掌握过滤和查找的技巧。若是开发者知道对象结构,并且也能够快速的找到想要的东西,那么找到那些异常情况将会变得容易一些。这里列出MAT工具全部内建的查询:
(【译者注】下面表格中的前两列都是MAT工具中菜单的名称)
查询: |
选项: |
描述: |
List objects |
With Outgoing References |
显示选中对象持有哪些对象. |
With Incoming References |
显示选中对象被哪些对象持有。[若是一个类有不少不须要的实例,那么能够找到哪些对象持有该对象,让这个对象无法被回收] |
|
Show object by class |
With Outgoing References |
显示选中对象持有哪些对象, 这些对象按类合并在一块儿排序 |
With Incoming References |
显示选中对象被哪些对象持有.这些对象按类合并在一块儿排序 |
|
Path to GC Roots |
With all references |
显示选中对象到GC根节点的引用路径,包括全部类型引用. |
Exclude weak references |
显示选中对象到GC根节点的引用路径,排除了弱引用. [弱引用不会影响GC回收对象] |
|
Exclude soft references |
显示选中对象到GC根节点的引用路径,排除软引用(【译者注】软引用持有的对象在内存空间足够时,GC不回收,内存空间足够时,GC回收) |
|
Exclude phantom references |
显示选中对象到GC根节点的引用路径,排除虚引用(【译者注】虚引用是最弱的引用,get()老是返回null,当它的对象被GC回收时,GC将reference放在ReferenceQueue中,用户代码当发现这个reference在在ReferenceQueue时就知道它持有的对象已经被回收了,这时能够作一些清理工做。《Java编程思想》第四版,中文版,第87页写到Java的finilize方法是为了对象被回收前作清理工做,可是事实上会有隐患,虚引用正是弥补) |
|
Merge Shortest Paths to GC Roots. |
选项和“Path to GC Roots”同样 | 显示GC根节点到选中对象的引用路径 |
Java Basics |
References Statistics Class Loader Explorer |
显示引用和对象的统计信息,列出类加载器,包括定义的类 |
Customized Retained Set |
计算选中对象的保留堆,排除指定的引用 |
|
Open in Dominator Tree |
对选中对象生成支配树 |
|
Show as Histogram |
展现任意对象的直方图 |
|
Thread Details |
显示线程的详细信息和属性 |
|
Thread Overview and Stacks |
- |
|
Java Collections |
Array Fill Ratio |
输出数组中,非基本类型、非null对象个数占数组总长度的比例。 |
Arrays Grouped by Size |
显示数组的直方图,按大小分组 |
|
Collection Fill Ratio |
输出给定集合中,非基本类型、非null对象个数占集合容量的比例。 |
|
Collections Grouped by Size |
显示集合的直方图,按大小分组 |
|
Extract Hash Set Values |
列出指定hash集合中的元素 |
|
Extract List Values |
列出指定LinkedList,ArrayList或Vector中的元素 |
|
Hash Entries |
展开显示指定HashMap或Hashtable中的键值对 |
|
Map Collision Ratio |
输出指定的映射集合的碰撞率 |
|
Primitive Arrays With a Constant Value |
列出基本数据类型的数组,这些数组是由一个常数填充的。 |
|
Leak Identification |
Component Report Top Consumers |
分析可能的内存浪费或者低效使用的组件,并输出最大的那个 |
报告(Reports)
MAT自带有一个报告生成系统,他能够自动分析dump文件而且生成报告给用户。第一种报告叫作“泄露疑点(Leak suspects)”,MAT分析dump文件,检查是否存在一些个头大的对象被一些引用持有保持活动状态,须要注意的是,泄露疑点并不必定是真的内存泄露。第二种报告叫作“顶级组件(Top Components)“,它包括可能的内存浪费信息,占用内存大的对象以及引用的统计信息。此报告对内存优化有很大帮助。
泄露疑点报告
泄露疑点报告包括一些潜在的内存泄露信息,再次强调一下,在这里列出的对象并不必定是真正的内存泄露,但它仍然是检查内存泄露的一个绝佳起点。报告主要包括描述和到达聚点的最短路径, 第三部分(每种类型积累的对象)主要是从第二部分衍生出来的(根据类型排序)。
图 13
“到聚点的最短路径” 很是有用,它可让开发人员快速发现究竟是哪里让这些对象没法被GC回收。
图. 14
使用MAT检测内存泄露
本小节主要介绍如何使用MAT检测内存泄露的实践部分,所以将会提供一段会形成内存泄露的代码做为例子。
会内存泄露的样例代码
第一个内存泄露例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
publicclassMainActivityextendsActivity {
//静态属性持有非静态内部类的实例--这么作很是糟糕
staticMyLeakedClass leakInstance =null;
@Override
publicvoidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Static field initialization
if(leakInstance ==null)
leakInstance =newMyLeakedClass();
ImageView mView =newImageView(this);
mView.setBackgroundResource(R.drawable.leak_background);
setContentView(mView);
}
/*
*非静态内部类
*/
classMyLeakedClass {
intsomeInt;
}
}
|
图. 17
从第一次旋转开始,每次都存在3mb的差别。每次旋转后,堆的大小都涨3mb。这是有什么东西不对劲的第一个警告,而后LogCat没有显示任何释放已分配内存的信号。这时就须要检查应用程序内部内存的状况了,运行DDMS,获取HPROF文件,而后用自带的MAT打开。
主屏幕上的确显示了有什么东西致使内存泄露了-它大概长的和图18差很少。
图. 18
饼图上显示,有很大一部份内存被资源文件占用-这很正常,由于任何应用都有一个GUI,可是本例子只有一个资源文件,所以问题应该应该隐藏在这里。还有两个FrameLayout实例(每一个有3mb)须要检查,开发者还能够沿着一些路径还检查内存泄露问题。
基于直方图的检查
图. 19
有三个比较大的bitmap对象,这么看来这个本例最坏可能有两到三个内存泄露。这几个对象的内存大小符合LogCat的输出,让咱们在检查一下他们到GC根节点的路径(剔除全部弱应用)。第一个bitmap对象看上去没什么问题,由于它只有一个应用指向本身,没有被任何其余对象引用,并且它正在等着被垃圾回收器回收。
图. 20
图 21
看来问题就出在剩下的第三个bitmap上了。它到GC根节点只有一条路径,并且它是被“leakInstance”对象持有的,正是leakInstance对象阻止了该bitmap对象被回收。
图. 22
同时,在路径上还有一个MainActivity对象 – 看到MainActivity对象不奇怪,由于每次旋转都会新建立一个Activity,让咱们看看到底发生了什么。首先经过正则表达式过滤器在直方图中找出MainActivity对象。
图. 23
图. 24
第一个MainActivity对象有一个引用指向context和ActivityThread,所以它看上去是如今正在显示的Activity。
图. 25
第二个对象只有一个引用指向本身,它正等着被垃圾回收,到目前为止,一切看上去都正常的。
图 26
如今再看第三个 – 就是它了!有个强引用指向leakInstance对象,就是它阻止了该对象被垃圾回收。
图. 27
基于支配树的检查
开发者能够经过不少种方法找到内存泄露。本文只能介绍其中几种,第二个要介绍的是基于支配树视图的。打开HPROF文件的支配树视图,按照保留堆大小进行排序。正如预料的同样,最上面的是资源类对象,还有三个FrameLayout类的对象(每一个3mb)以及一个Bitmap对象(1mb)。FrameLayout对象看上去嫌疑很大,所以咱们首先检查它们。由于支配树已经列出了具体的对象,所以咱们能够直接查看它们到GC根节点的路径。
图. 28
第一个对象就是问题所在!它到GC根节点的惟一路径正是leakInstance对象,所以它是一个泄露。
图. 29
第二个和第三个对象分别是当前正在显示和正在等着垃圾回收的。
图. 30
让咱们在看看那个bitmap对象,它也有多是一个内存泄露。选择android.graphic.Bitmap,选择显示到GC根节点的路径,剔除全部弱引用。
图. 31
bitmap类型有三个对象,每一个对象到GC根节点的路径均可以查看到,上面说的状况再次重演,三个实例中的两个很显然没问题,可是第三个对象指向leakInstance,它一直都是活动状态,不会被回收。
图. 32
可能还有上百条路径能够顺藤摸瓜找出最终的泄露点,应该选择哪条路径取决于不一样的开发者了,不一样的开发人员有对如何分析内存有着不一样的看法。
第二个内存泄露例子
第二个内存泄露场景发生在application context上。它将application context传递给一个单例模式的类,并将其做为一个属性保留下来。这个操做将会使得MainActivity没法被垃圾回收。将context做为静态属性保存也会致使一样的结果,所以这种作法应该避免。为了不重复罗嗦,这里只介绍一种查找内存泄露的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
publicclassMainActivity2extendsActivity {
SingletonClass mSingletonClass =null;
@Override
publicvoidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSingletonClass = SingletonClass.getInstance(this);
}
}
classSingletonClass {
privateContext mContext =null;
privatestaticSingletonClass mInstance;
privateSingletonClass(Context context) {
mContext = context;
}
publicstaticSingletonClass getInstance(Context context) {
if(mInstance ==null) {
mInstance =newSingletonClass(context);
}
returnmInstance;
}
}
|
图. 33
概览界面并无提供什么重要信息,所以开发人员须要继续本身的探索。这个例子中没有bitmap和其余资源,可是直方图显示这里有不少MainActivity对象 – 检查检查它们也许能获得更多更有价值的消息。
图. 34
将手机旋转3次,直方图显示这里有4个MainActivity对象。嗯,是时候检查是否是有哪一个对象阻止它们被回收了。要作到这一点,首先列出全部有incomming refrence的对象。只须要展开视图就很容易发现第一个对象就是当前正在显示的Activity(他包含指向ActivityThread的引用)。
图. 35
继续列出其余两个对象的到GC根节点的路径。其中一个只有一个引用指向它本身,另一个指向mInstance,该引用在SignletonClass中,还有一个应用指向当前显示的Activity(从mSigletonClass)。这正是一个泄露。
图. 36
很明显能够看出context让垃圾回收没法回收该对象。另外还有一个问题 – 每次建立一个Acitivity实例的时候,context都被传递给SingletonClass。这是个严重的问题,由于context引用指向一个不在须要的Activity,从而让这个Activity保持活跃没法被回收。在比较大的项目中,这中状况可能会致使一些意象不到的行为,而且这种问题很难被检查出来。