分享

Android内存分析和调优(中)

 univasity 2014-07-07

前文中讨论了如果使用adb shell procrank, dumpsys meminfo和showmaps分析进程的内存占用情况。


本文将继续细化,具体分析导致内存过大的dalvik heap。


Dalvik heap分析和优化


Dalkvik heap是最常见的android应用内存优化的对象。


通过上文的分析,我们可以通过adb shell的命令,知道用了多少dalvik heap。在ADT的eclipse的DDMS视图,可以更细致的查看这些内存用到什么地方。
参考DDMS使用说明(搜索viewing heap),我们可以首先在devices view中选中一个进程,然后enable "update heap“(不带红箭头的半杯水图标),之后在heap view中点击”Cause GC"。这样子除了Heap Size, Allocated, Freed,还可以看到data object,class object,和n-byte array分别占用的内存大小。


不过真心说,这个还是太粗糙了,没法精确到具体的类。此时大名鼎鼎的MAT就派上用场了。


MAT是对java内存镜像进行分析的工具。所以首先需要导出进程的内存镜像,可以在DDMS上的device view点击Dump HPROF file(带红箭头的半杯水图标),生成hprof文件。因为android的文件格式跟通用的java的hprof格式不一样,还需要通过hprof-conv命令来转换。然后就可以用MAT来打开。


看起来挺麻烦的。事实上,现在MAT的eclipse插件可以把上面的工具一键完成。只需要点击Dump HPROF file图标,然后MAT插件就会自动转换格式,并且在eclipse中打开分析结果。eclipse中还专门有个Memory Analysis视图,可以更详细的查看MAT的分析结果。


MAT可以根据内存镜像,以可视化的方式告诉我们哪个类,哪个对象分配了多少内存。但如果只是这样,用处就没那么大了。因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[]。所以我们如果只看对象本身的内存,那么数量都很小。我们称之位shallow heap。


于是MAT提出了Retained Heap的概念,它表示如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。这里要说一下的是,Retained Heap并不总是那么有效。例如我在A里new了一块内存,赋值给A的一个成员变量。此时我让B也指向这块内存。此时,因为A和B都引用到这块内存,所以A释放时,该内存不会被释放。所以这块内存不会被计算到A或者B的Retained Heap中。为了纠正这点,MAT中的Leading Object(例如A或者B)不一定只是一个对象,也可以是多个对象。此时,(A, B)这个组合的Retained Set就包含那块大内存了。对应到MAT的UI中,在Histogram中,可以选择Group By class, superclass or package来选择这个组。(又开始Histogram中不显示Retained heap,需要点击那个计算器的按钮才会计算出来)。这里最小的粒度是类级别的。


为了计算Retained Memory,MAT引入了Dominator Tree。加入对象A引用B和C,B和C又都引用到D(一个菱形)。此时要计算Retained Memory,A的包括A本身和B,C,D。B和C因为共同引用D,所以他俩的Retained Memory都只是他们本身。D当然也只是自己。我觉得是为了加快计算的速度,MAT改变了对象引用图,而转换成一个对象引用树。在这里例子中,树根是A,而B,C,D是他的三个儿子。B,C,D不再有相互关系。把引用图变成引用树,计算Retained Heap就会非常方便,显示也非常方便。对应到MAT UI上,在dominator tree这个view中,显示了每个对象的shallow heap和retained heap。然后可以以该节点位树根,一步步的细化看看retained heap到底是用在什么地方了。要说一下的是,这种从图到树的转换确实方便了内存分析,但有时候会让人有些疑惑。本来对象B是对象A的一个成员,但因为B还被C引用,所以B在树中并不在A下面,而很可能是平级。


为了纠正这点,MAT中点击右键,可以List objects中选择with outgoing references和with incoming references。这是个真正的引用图的概念,表示该对象的出节点(被该对象引用的对象)和入节点(引用到该对象的对象)。


另外一个类似的功能是右键菜单的Path to GC RootsGC roots是可能导致GC的节点。这个Path则是从这些GC root节点中的某个到当前对象的最短引用路径。对这个如何计算不是很确定,我想应该是根据引用树而不是dominator tree。后面会看到这个功能在非常的有用。


说完工具,下面是具体的减少内存大小。一般要解决两个问题:内存泄露和释放暂时不需要的内存。


Java内存泄露归根结底都是一个原因导致的,应该被释放的对象被生命期更长的对象引用,所以没法被GC。这个生命期更长的对象很常见的是static对象,会持续整个进程。在个人实际工作中,我会先用adb shell dumpsys meminfo查看dalvik heap会不会持续增长。如果是,我会在在dominator Tree中按照Retained Memory排序,找出比较大的(经常是Bitmap),然后用Path to GC Roots看看其引用情况。在这个Path中,一般会发现我们app自己包的类,可以分析这个类是不是还是需要的。如果不需要,那说明可能存在内存泄露。此时,在对这个自己包的类查看incoming references。看看到底是哪些引用导致它没有释放。用这种方法,会比较快的发现问题。MAT自己也提供了智能的内存分析工具,我没有用,不好评论。


一个制造内存泄露的很有效的办法是不断的切换横屏和竖屏。现实中很多内存泄露都是因为static的对象指向了Activity对象(作为context传),而切换横屏和竖屏会导致Activity重新生成。所以如果有问题,内存很快就会变大。从编码上讲,avoid-memory-leak这篇文章教育我们,在需要context的地方,尽量使用getApplicationContext,而不是Activity本身。


另外一个可以减少内存的方法是删除临时不用的内存。编码中可能是为了内存cache以提高性能,可能只是偷懒,之前场景使用的内存并没有被释放掉。这样子下次再回到这个场景,会快一点;但会可能会占用不少内存。我觉得在android这类内存受限的系统上,还是应该谨慎使用控件换时间的策略。如果想删除临时不用的内存,也可以使用mat像监测内存泄露一样,看看哪些比较大的内存临时不用却仍然被引用,然后删除对其引用。


关于mat的一个小技巧是mat经常发现比较大的内存泄露是图片,此时如果知道图片是什么内容就很容易定位到何时导致的内存泄露。这个帖子回答了这个问题。


关于dalvik mat最后再推荐自己看的一个android memory manage video(slides , contentcontent2)。里面对MAT和内存泄露都有介绍。这个blog也是对二者都有介绍,很好。关于MAT更好的文档集合在这里,MAT作者写的。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多