Android内存监控与分析(三):内存分析及原理

Android内存监控与分析(三):内存分析及原理

APP测试中难免会有各种显式或者隐式的内存泄漏(Memory Leak)问题,如果不及时发现处理,可能会因为内存泄漏导致各种奇怪的问题(如,卡顿和闪退),甚至可能出现因内存不足(Out of Memory,简称OOM)而导致APP崩溃。
本文将通过实战分析内存泄漏和内存溢出问题,并在必要时说明原理或机制。结构分为四个模块,如图1:

Android内存监控与分析(三):内存分析及原理_第1张图片
图1 内存监控与分析

三、内存分析及原理

针对hprof文件,看下到底是哪些对象更多,占用的内存更大;这块需要和开发一起分析,也是最难的部分。要分析内存泄漏的原因,我们可以从代码分析内存泄漏的根本原因:因为引用未释放。即,内存泄漏:
当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种情况导致了本该被回收的对象不能被回收而停留在堆内存中,这样就可能出现内存泄漏。
说到引用与回收,需要谈谈Java内存管理、垃圾回收(GC)机制和Android的内存管理,从而知道其内在的联系。

(一)Java内存管理机制
Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如图11所示:

Android内存监控与分析(三):内存分析及原理_第2张图片
图11 Java虚拟机运行时数据区

程序计数器:一块较小内存区域,指向当前所执行的字节码。如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空。

Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的Native方法服务。
Java堆: 是虚拟机管理内存中最大的一块,被所有线程共享,该区域用于存放对象实例,几乎所有的对象都在该区域分配。Java堆是内存回收的主要区域,因此很多时候也被称作“GC堆”。从内存回收角度看,由于现在的收集器大都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代,再细分一点的话可以分为Eden空间、From Survivor空间、To Survivor空间等。根据Java虚拟机规范规定,Java堆可以处于物理上不连续的空间,只要逻辑上是连续的就行。 如果在堆中没有内存完成实例分配,并且也无法再扩展时,将会抛出OutofMemoryError异常。
方法区:与Java堆一样,是各个线程所共享的内存区域,用于存储已被虚拟机加载类信息、常亮、静态变量、即时编译器编译后的代码等数据。
运行时常量池:运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。

(二)Java垃圾回收(GC)机制
从所周知,内存管理一直是编程的一大难题。例如,C/C++语言,内存管理是显式的,也就是说程序猿自己申请内存,自己释放内存。如果程序猿忘记或疏忽没有释放内存,那么就会产生内存泄漏。所以,Java语言引入了内存自动管理机制,即垃圾回收机制(GC,就是Garbage Collection的缩写)。但是,内存自动回收机制可以解决大部分问题,却不能解决所有问题。
Java堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定哪些对象还“活着”,哪些已经“死亡”,也就是不会被任何途径所使用的对象。

  1. Java垃圾回收机制
    A.分配内存;
    B.确保任何被引用的对象存储在内存中;
    C.回收引用不可达的对象的内存。

  2. GC Root与可达性分析算法
    这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到这个对象不可达)时,则证明此对象是无用的,是可回收的垃圾对象。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。如图12:

Android内存监控与分析(三):内存分析及原理_第3张图片
图12 GC Root与可达性分析算法

3.引用
通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
在JDK 1.2之后,Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用, 只要强引用还存在,垃圾收集器永远不会回收掉被强引用的对象。当进行编码:obj = null,此时,刚刚在堆中分配地址并新建的obj对象没有其他的任何引用,当系统进行垃圾回收时,堆内存将被垃圾回收。
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 发现内存不足时,回收软引用。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 即,只要触发GC,马上回收软引用。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 同样只要触发GC,马上回收虚引用。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

知道三者之间的关系之后,我们来看一下垃圾回收的正常内存释放过程,对比垃圾回收前后的对象,如图13:

Android内存监控与分析(三):内存分析及原理_第4张图片
图13 正常回收

这里GC Roots表示垃圾回收器对象,每个节点表示内存中的对象,箭头表示对象之间的引用关系,能被GC Roots直接或者间接引用到的对象ABCD,表示正在使用的对象,不被引用的对象EFG是无用对象,GC时就会被回收掉。当系统触发一次GC时,对象EFG就会被回收。
如果当连续多次打开APP,界面卡顿,初步推测应用中可能存在内存泄漏。对比未释放对象过程,如图14:

Android内存监控与分析(三):内存分析及原理_第5张图片
图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内存监控与分析(三):内存分析及原理_第6张图片
图15 由Zygote服务进程孵化的APP进程

那么Android是怎么管理这些App的内存的呢, 这些独立运行的VM中的内存管理又是怎样的呢?

1.Android虚拟机类型:Dalvik & ART

A. Dalvik:Android 4.4及其以下平台使用的虚拟机
B. ART:Android 4.4以上平台使用的虚拟机
Android使用Java开发,不过Android平台不用Java虚拟机(VM)来执行代码,而且把APP编译成Dalvik字节码,使用Dalvik虚拟机来执行。Java代码仍然编译成Java字节码,但随后Java字节码会被dex编译器(dx,SDK工具)编译成Dalvik字节码。最终,APP只包含Dalvik字节码,而不是Java字节码。Dalvik在应用程序启动时,JIT通过进行连续的性能分析来优化程序代码的执行, 在程序运行的过程中,Dalvik虚拟机在不断的进行将字节码编译成机器码的工作。
ART 取自 Android Run Time。 Android用其取代Dalvik,,主要目的就是为了提升运行性能。 ART引入了AOT这种预编译技术,在应用程序安装的过程中,ART就已经将所有的字节码重新编译成了机器码。应用程序运行过程中无需进行实时的编译工作,只需要进行直接调用。因此,ART极大的提高了应用程序的运行效率,同时也减少了手机的电量消耗,在垃圾回收等机制上也有了较大的提升。所以,ART相比Dalvik有了几个关键的提升。数据对比,如表1:
对比项目 CPU RAM内存 ROM内存 流畅度 省电 APP加载速度 兼容性
ART模式 更佳 更佳 有待优化
Dalvik模式 普通 普通
表1 虚拟机数据对比

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)

Android内存监控与分析(三):内存分析及原理_第7张图片
图16 Dalvik的Heap和Stack

了解生成的数据在哪里存储之后,才能更好的排除问题。

查看APP内存使用情况,命令adb shell dumpsys meminfo packageor pid。如图17:

Android内存监控与分析(三):内存分析及原理_第8张图片
图17 内存信息(dumpsys meminfo package)

其中,各个字段的含义如图18:

Android内存监控与分析(三):内存分析及原理_第9张图片
图18 dumpsys meminfo中各字段的含义

通过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:感谢光荣之路的追逐和悟空老师悉心的指导和热情的帮助,让我获益匪浅!

你可能感兴趣的:(性能测试,Android)