Android内存监控与分析(三):内存分析及原理
APP测试中难免会有各种显式或者隐式的内存泄漏(Memory Leak)问题,如果不及时发现处理,可能会因为内存泄漏导致各种奇怪的问题(如,卡顿和闪退),甚至可能出现因内存不足(Out of Memory,简称OOM)而导致APP崩溃。
本文将通过实战分析内存泄漏和内存溢出问题,并在必要时说明原理或机制。结构分为四个模块,如图1:
三、内存分析及原理
针对hprof文件,看下到底是哪些对象更多,占用的内存更大;这块需要和开发一起分析,也是最难的部分。要分析内存泄漏的原因,我们可以从代码分析内存泄漏的根本原因:因为引用未释放。即,内存泄漏:
当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种情况导致了本该被回收的对象不能被回收而停留在堆内存中,这样就可能出现内存泄漏。
说到引用与回收,需要谈谈Java内存管理、垃圾回收(GC)机制和Android的内存管理,从而知道其内在的联系。
(一)Java内存管理机制
Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如图11所示:
程序计数器:一块较小内存区域,指向当前所执行的字节码。如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空。
(二)Java垃圾回收(GC)机制
从所周知,内存管理一直是编程的一大难题。例如,C/C++语言,内存管理是显式的,也就是说程序猿自己申请内存,自己释放内存。如果程序猿忘记或疏忽没有释放内存,那么就会产生内存泄漏。所以,Java语言引入了内存自动管理机制,即垃圾回收机制(GC,就是Garbage Collection的缩写)。但是,内存自动回收机制可以解决大部分问题,却不能解决所有问题。
Java堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定哪些对象还“活着”,哪些已经“死亡”,也就是不会被任何途径所使用的对象。
Java垃圾回收机制
A.分配内存;
B.确保任何被引用的对象存储在内存中;
C.回收引用不可达的对象的内存。
GC Root与可达性分析算法
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到这个对象不可达)时,则证明此对象是无用的,是可回收的垃圾对象。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。如图12:
知道三者之间的关系之后,我们来看一下垃圾回收的正常内存释放过程,对比垃圾回收前后的对象,如图13:
这里GC Roots表示垃圾回收器对象,每个节点表示内存中的对象,箭头表示对象之间的引用关系,能被GC Roots直接或者间接引用到的对象ABCD,表示正在使用的对象,不被引用的对象EFG是无用对象,GC时就会被回收掉。当系统触发一次GC时,对象EFG就会被回收。
如果当连续多次打开APP,界面卡顿,初步推测应用中可能存在内存泄漏。对比未释放对象过程,如图14:
图13演示的GC过程跟图12很像,不过这时候再触发GC时,EG会被回收,F对于应用来说虽然无用了,却无法被回收,因为未释放引用。最后导致了内存泄漏。
因此,内存泄漏的根本原因是当引用对象在使用完毕后未释放,结果导致一直占据该内存单元 ,直到程序结束。
(二)Android的内存管理
找到内存泄漏的原因后,我们需要进一步知道Android是如何管理APP内存的。
在APP启动时,为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的虚拟机(VM)实例来运行,**它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的独立进程中运行的(App heap)。**Android为不同类型的进程分配了不同的内存使用上限(图3),如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存溢出,从而被kill掉,这使得仅仅APP自己的进程被kill掉(图4),而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。如图15:
那么Android是怎么管理这些App的内存的呢, 这些独立运行的VM中的内存管理又是怎样的呢?
1.Android虚拟机类型:Dalvik & ART
对比项目 | CPU | RAM内存 | ROM内存 | 流畅度 | 省电 | APP加载速度 | 兼容性 |
---|---|---|---|---|---|---|---|
ART模式 | – | 小 | 大 | 更佳 | 更佳 | 慢 | 有待优化 |
Dalvik模式 | – | 大 | 小 | 普通 | 普通 | 块 | 好 |
ART优点:
系统性能的显著提升;
应用启动更快、运行更快、体验更流畅、触感反馈更及时;
更长的电池续航能力;
支持更低的硬件。
ART缺点:
更大的存储空间占用,可能会增加10% - 20%;
更长的应用安装时间。
2.Android的内存管理方式
ART和Dalvik都是使用paging和memory-mapping(mmapping)来管理内存的。这就意味着,任何被分配的内存都会持续存在,唯一释放这块内存的方式就是释放对象引用(让对象GC Root不可达),从而让GC来回收内存(参考前文关于Java垃圾回收机制)。
对于每个App进程来说, Heap内存被限制在一个虚拟的内存区间内。且定义了逻辑上的使用的Heap Size,这个Heap Size在系统限制的最大值之内(图3)随着应用的使用情况而变化(图2)。
Dalvik的Heap和Stack(如图16)
了解生成的数据在哪里存储之后,才能更好的排除问题。
查看APP内存使用情况,命令adb shell dumpsys meminfo packageor pid。如图17:
图17 内存信息(dumpsys meminfo package)
其中,各个字段的含义如图18:
通过dumpsys meminfo获取的信息中,主要关注如下几个字段(图18)
1)Native/Dalvik 的Heap 信息
具体在上面的第一行和第二行(图17中上红框),它分别给出的是JNI层和Java层的内存分配情况,如果发现这个值一直增长,则代表程序可能出现了内存泄漏。前文说过,Java堆(Heap)也叫GC堆。结合图17,数据在Dalvik堆(Heap)中的存储,可以帮助定位存储在内存中的目标数据类型。
2)Total 的PSS信息
这个值(图17中下红框)就是APP真正占据的内存大小,通过这个信息,你可以轻松判别手机中哪些程序占内存比较大了。
3. Dalvik堆(Heap)的常见问题
随着测试的执行,随之而来的就是一大堆产生的数据。对产生的数据进行分析,找出可能存在的问题,以及问题可能的原因是接下来的重点。
常见的现象有以下几种:
1)随着功能的反复执行,Heap内存一直在持续增长。这种情况通常是出现了内存泄漏,这种情况最适合用LeakCanary等泄漏检查工具进行白盒测试分析。
2)代码执行时出现了频繁的GC,Heap Alloc内存大幅度波动。这种情况通常是分配了许多临时变量或数组,随后又被迅速回收,这种情况在确定具体场景后适合使用Heap Viewer / Allocation Tracker等工具来查看具体分配的对象。
3)每次启动应用后,Heap内存相比以前版本稳定增长。这种情况通常出现在启动后待机或使用某功能后,可能是由新功能及代码改动引入的固定内存增长。这种情况适合获取Heap Dump后进行多版本或功能使用前后的对此,能够迅速找到增长原因。
4)Heap Alloc变化不大,但进程的Dalvik Heap Pss(Proportional Set Size)内存明显增加。这种情况比较少见,是由于分配了大量小对象造成的内存碎片。
参考资料:
1.《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明 著,机械工业出版社
2.《移动App性能评测与优化》,TMQ专项测试团队 编著,机械工业出版社
3.文档,Android内存分析指南,追逐 编
PS:感谢光荣之路的追逐和悟空老师悉心的指导和热情的帮助,让我获益匪浅!