从早期G1的192MB RAM开始,到现在动辄1G -2G RAM的设备,为单个App分配的内存从16MB到48MB甚至更多,但OOM从不曾离我们远去。这是因为大部分App中图片内容占据了50%甚至75%以上,而App内容的极大丰富,所需的图片越来越多,屏幕尺寸也越来越大分辨率也越来越高,所需的图片的大小也跟着往上涨,这在大屏手机和平板上尤其明显。而且还经常要兼容低版本的设备。所以Android的内存管理显得极为重要。
在这里我们主要讲两件事情:
1.Gingerbread和Honeycomb中的一些影响你使用内存的变化
-heap size
-GC
-bitmaps
2.理解heap的用途分配
-logs
-merory leaks
-Eclispe Memory Analyzer(MAT)
首先第一部分,我们都知道Android是个多任务操作系统,同时运行着很多程序,都需要分配内存,不可能为一个程序分配越来越多的内存以至于让整个系统都崩溃,因此heap的大小有个硬性的限制,跟设备相关,从发展来说也是越来越大,G1:16MB,Droid:24MB,Nexus One:32MB,Xoom:48MB,但是一旦超出了这个使用的范围,OOM便产生了。如果你正在开发一个应用,想知道设备的heap大小的限制是多少,比方说根据这个值来估算自己应用的缓存大小应该限制在什么样一个水平,你可以使用ActivityManager.getMemoryClass ()来获得一个单位为MB的整数值,一般来说最低不少于16MB,对于现在的设备而言这个值会越来越大,24MB,32MB,48MB甚至更大。
但是对于一些内存非常吃紧的比如图片浏览器等应用,在平板上所需的内存更大了。因此在Honeycomb之后AndroidManifest.xml增加了largeHeap的选项
这允许你的应用使用更多的heap,可以用ActivityManager.getLargeMemoryClass ()返回一个更大的可用heap size。但是这里要警告的是,千万不要因为你的应用报OOM了而使用这个选项,因为更大的heap size意味着更多的GC时间,意味着应用的性能越来越差,而且用户也会发现其他应用很有可能会内存不足。只有你需要使用很多的内存而且非常了解每一部分内存的用途,这些所需的内存都是不可或缺的,这个时候你才应该使用这个选项。
刚刚我们提到更大的heap size意味着更多的GC时间,下面我们来谈谈Garbage Collection。
如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点,比方说thread stack中的变量,JNI中的全局变量,zygote中的对象等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。如下图蓝色部分。
因此也可以看出,更大的heap size需要遍历的对象更多,回收垃圾的时间更长,所以说使用largeHeap选项会导致更多的GC时间。
在Gingerbread之前,GC执行的时候整个应用会暂停下来执行全面的垃圾回收,因此有时候会看到应用卡顿的时间比较长,一般来说>100ms,对用户而言已经足以察觉出来。Gingerbread及以上的版本,GC做了很大的改进,基本上可以说是并发的执行,也不是执行完全的回收,只有在GC开始以及结束的时候会有非常短暂的停顿时间,一般来说<5ms,用户也不会察觉到。
在Honeycomb之前,Bitmap的内存分配如下图。
蓝色部分是Dalvik heap,黄色部分是Bitmap引用对象的堆内存,而Bitmap实际的Pixel Data是分配在Native Memory中。这样做有几个问题,首先需要调用reclyce()来表明Bitmap的Pixel Data占用的内存可回收,不调用这个方法的话就要靠finalizer来让GC回收这部分内存,但了解finalizer的应该都知道这相当的不可靠;其次是很难进行Debug,因为一些内存分析工具是查看不到Native Memory的;再次就是不调用reclyce()需要回收Native Memory中的内存的话会导致一次完整的GC,GC执行的时候会暂停整个应用。
Honeycomb之后,Bitmap的内存分配做出了改变,如下图
蓝色黄色部分没有变化,但Bitmap实际的Pixel Data的内存也同样分配在了Dalvik heap中。这样做有几个好处。首先能同步的被GC回收掉;其次Debug变得容易了,因为内存分析工具能够查看到这部分的内存;再次就是GC变成并发了,可做部分的回收,也就是极大缩短了GC执行时暂停的时间。
接下来我们讲第二部分。一般来说我们希望了解我们应用内存分配,最基本的就是查看Log信息。比方说看这样一个Log信息(这是Gingerbread版本的,Honeycomb以后log信息有改动):
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/
9991K, external 4703K/5261K, paused 2ms 2ms
GC_XXX表明是哪类GC以及触发GC的原因。几种GC类型:
- GC_CONCURRENT:这是因为你的heap内存占用开始往上涨了,为了避免heap内存满了而触发执行的。
- GC_FOR_MALLOC:这是由于concurrent gc没有及时执行完而你的应用又需要分配更多的内存,内存要满了,这个时候不得不停下来进行malloc gc。
- GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC,也就是上文提到的Bitmap Pixel Data之类的。
- GC_HPROF_DUMP_HEAP:这是当你做HPROF这样一个操作去创建一个HPROF profile的时候执行的。
- GC_EXPLICIT:这是由于你显式的调用了System.gc(),这是不提倡的,一般来说我们可以信任系统的GC。
freed 2049K表明在这次GC中回收了多少内存。
65% free 3571K/9991K是heap的一些统计数据,表明这次回收后65%的heap可用,存活的对象大小3571K,heap大小是9991K。
external 4703K/5261K是Native Memory的数据。放Bitmap Pixel Data或者是NIO Direct Buffer之类的。第一个数字表明Native Memory中已分配了多少内存,第二个值有点类似一个浮动的阀值,表明分配内存达到这个值系统就会触发一次GC进行内存回收。
paused 2ms 2ms表明GC暂停的时间。从这里你可以看到越大的heap size你需要暂停的时间越长。如果是concurrent gc你会看到2个时间一个开始一个结束,这时间是很短的,但如果是其他类型的GC,你很可能只会看到一个时间,而这个时间是相对比较长的。
通过Log可以对内存信息有个基本的了解,但这不足以了解什么对象在使用内存,在哪使用了内存。这时候你需要用Heap Dumps。一个Heap Dump基本上来说就是一个包含你heap中所有对象信息的二进制文件。你可以用DDMS来生成这个文件,点击DDMS中下图的那个按钮。
同时Heap Dumps也有对应的API,你想要在特定的时间点获取一份Heap Dump,使用android.os.Debug.dumpHprofData()。获取到的文件要转换成标准的HPROF格式,使用如下命令:hprof-conv orig.hprof converted.hprof。然后用MAT或者jhat进行分析。
在讲MAT之前先讲下Memory Leaks。要清楚GC并不能防止Memory Leaks,所谓Memory Leaks就是引用到了已经没用的对象从而让这些对象避免了被GC回收,跟C/C++中的概念并不一样。容易导致内存泄漏的是一些Activity,Context,View,Drawable之类的引用,和一些非静态的内部类比方说Runnable之类的以及一些Caches。比如你旋转屏幕的时候在新的方向上产生一个新的Activity,如果有变量引用到旧的Activity就会导致其无法被GC,造成Memory Leaks。
通常通过上面介绍的Log信息,只要已用memory一直处于上升的情形而不回落,便大致能了解到应用存在Memory Leaks。不过MAT这类工具可以帮助你更好的对memory进行分析。使用MAT之前有2个概念是要掌握的:Shallow heap和Retained heap。Shallow heap表示对象本身所占内存大小,一个内存大小100bytes的对象Shallow heap就是100bytes。Retained heap表示通过回收这一个对象总共能回收的内存,比方说一个100bytes的对象还直接或者间接地持有了另外3个100bytes的对象引用,回收这个对象的时候如果另外3个对象没有其他引用也能被回收掉的时候,Retained heap就是400bytes。
MAT使用Dominator Tree这样一种来自图形理论的概念。
所谓Dominator,就是Flow Graph中从源节点出发到某个节点的的必经节点。那么根据这个概念我们可以从上图左侧的Flow Graph构造出右侧的Dominator Tree。这样一来很容易就看出每个节点的Retained heap了。Shallow heap和Retained heap在MAT中是非常有用的概念,用于内存泄漏的分析。
我们用Honeycomb3.0中的HoneycombGallery做一个Demo。在工程的MainActivity当中加入如下代码:
上面这段代码,对Java熟悉的同学都应该了解内部类对象持有了外部类对象引用,而leak作为静态变量在非空判断下只产生了一个对象,因此当旋转屏幕时生成新的Activity的时候旧的Activity的引用依然被持有,如下图:
通过观察旋转屏幕前后Log中GC的信息也能看出heap的分配往上涨了许多,并且在GC执行完heap的分配稳定之后并没有降下来,这就是内存泄漏的迹象。
我们通过MAT来进行分析。先下载MAT,可以作为Eclipse插件下载,也可以作为RCP应用下载,本质上没有区别。DDMS中选中应用对应的进程名,点击Dump HPROF file的按钮,等一小段时间生成HPROF文件,如果是Eclipse插件的话,Eclipse会为这个HPROF自动转化成标准的HPROF并自动打开MAT分析界面。如果是作为RCP应用的话,需要用sdk目录tools中的hprof-conv工具来进行转化,也就是上文提及的命令hprof-conv orig.hprof converted.hprof,这种方式保存HPROF文件的位置选择更为自主,你也可以修改Eclipse的设置让Eclipse提示保存而不是自动打开,在Preferences -> Android -> DDMS中的HPROF Action由Open in Eclipse改为Save to disk。打开MAT,选择转化好的HPROF文件,可以看到Overview的界面如下图:
中间的饼状图就是根据我们上文所说的Retained heap的概念得到的内存中一些Retained Size最大的对象。点击饼状图能看到这些对象类型,但对内存泄漏的分析还远远不够。再看下方Action中有Dominator Tree和Histogram的选项,这一般来说是最有用的工具。还记得我们上文说过的Dominator Tree的概念吗,这就是我们用来跟踪内存泄漏的方式。点开Dominator Tree,会看到以Retained heap排序的一系列对象,如下图:
Resources类型对象由于一般是系统用于加载资源的,所以Retained heap较大是个比较正常的情况。但我们注意到下面的Bitmap类型对象的Retained heap也很大,很有可能是由于内存泄漏造成的。所以我们右键点击这行,选择Path To GC Roots ->exclude weak references,可以看到下图的情形:
Bitmap最终被leak引用到,这应该是一种不正常的现象,内存泄漏很可能就在这里了。MAT不会告诉哪里是内存泄漏,需要你自行分析,由于这是Demo,是我们特意造成的内存泄漏,因此比较容易就能看出来,真实的应用场景可能需要你仔细的进行分析。
根据我们上文介绍的Dominator的概念,leak对象是该Bitmap对象的Dominator,应该出现在Dominator Tree视图里面,但实际上却没有。这是由于MAT并没有对weak references做区别对待,这也是我们选择exclude weak references的原因。如果我们Path To GC Roots ->with all references,我们可以看到下图的情形:
可以看到还有另外一个对象在引用着这个Bitmap对象,了解weak references的同学应该知道GC是如何处理weak references,因此在内存泄漏分析的时候我们可以把weak references排除掉。
有些同学可能希望根据某种类型的对象个数来分析内存泄漏。我们在Overview视图中选择Actions -> Histogram,可以看到类似下图的情形:
上图展示了内存中各种类型的对象个数和Shallow heap,我们看到byte[]占用Shallow heap最多,那是因为Honeycomb之后Bitmap Pixel Data的内存分配在Dalvik heap中。右键选中byte[]数组,选择List Objects -> with incoming references,可以看到byte[]具体的对象列表:
我们发现第二个byte[]的Retained heap较大,内存泄漏的可能性较大,因此右键选中这行,Path To GC Roots -> exclude weak references,同样可以看到上文所提到的情况,我们的Bitmap对象被leak所引用到,这里存在着内存泄漏。
在Histogram视图中第一行<Regex>中输入com.example.android.hcgallery,过滤出我们自己应用中的类型,如下图:
我们发现本应该只有一个MainActivity现在却有两个,显然不正常。右键选择List Objects -> with incoming references,可以看到这两个具体的MainActivity对象。右键选中Retained heap较大的MainActivity,Path To GC Roots -> exclude weak references,再一次可疑对象又指向了leak对象。
以上是MAT一些基本的用法,如果你感兴趣,可以自行深入的去了解MAT的其他功能。