MobDevGroupAndroidandroid高阶

Android性能优化(三)之内存管理

2017-02-28  本文已影响4641人  未来的理想

1、初识内存优化

在Android的性能优化的各个部分里,内存的问题绝对是最令人头疼的一部分,虽然Android有垃圾自动回收机制不需要手动干预,但也恰因为此,出现内存问题如内存泄漏和内存溢出等,如果对内存管理机制不熟悉,会更加难以排查问题。

因为内存方面的知识较多且不易理解,内存优化部分就分两篇文章进行,本文主要是关于Java、Android的内存分配、回收、GC等理论知识。

2、内存分配

谈Android的内存,就不能不提Java的内存管理。Java程序在运行的过程中会将其管理的内存分为若干个不同的数据区:

JVM运行时数据区

方法区:方法区存放的是类信息、常量、静态变量,所有线程共享区域。

虚拟机栈:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,线程私有区域。

本地方法栈:与虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务

JVM管理的内存中最大的一块,所有线程共享;用来存放对象实例,几乎所有的对象实例都在堆上分配内存;此区域也是垃圾回收器(Garbage Collection)主要的作用区域,内存泄漏就发生在这个区域

程序计数器可看做是当前线程所执行的字节码的行号指示器;如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。

备注:
有一种习惯说法:把Java的内存区域分为堆内存(Heap)和栈内存(Stack),Stack访问快,Heap访问慢,Stack中保存的是对象的引用(指针),Heap中保存的是对象的实例。

实际上这种说法是笼统、粗糙的,此处所说的Stack仅仅是虚拟机栈中的局部变量表部分。虚拟机栈与JVM运行时数据区涵盖的都比此种说法多。

3、内存回收

3.1标记-清除算法

最基础的收集算法:分为“标记”和“清除”两个阶段,首先,标记出所有需要回收的对象,然后统一回收所有被标记的对象。
这种方法有两个不足点:

  1. 效率问题,标记和清除两个过程的效率都不高;
  2. 空间问题,标记清除之后会产生大量的不连续的内存碎片。
“标记-清除”算法示意图

3.2复制算法

将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存将用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉。
这种方法的特点:

“复制”算法示意图

3.3标记-整理算法

先标记需要回收的对象(标记过程与“标记-清除”算法一样),然后把所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
这种方法的特点:

“标记-整理”算法示例图.png

3.4分代收集算法

根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

4、对象是否回收的依据

4.1引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用该对象时,计数器值加1;引用失效时,计数器值减1;任意时刻计数器为0的对象就是不可能再被使用的,表示该对象不存在引用关系。
这种方法的特点:

4.2可达性分析算法

以一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达),则证明此对象是不可用的。

可达性分析算法判定对象是否可回收

5、Android的内存管理

Android系统的ART和Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色, 使用pagingmemory-mapping来管理内存,这意味着不管是因为创建对象还是使用使用内存页面造成的任何被修改的内存,都会一直存在于内存中,App唯一释放内存的方法就是释放App持有的对象引用,使GC可以回收。

Android Runtime内存堆划分

在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Generation区域的gc操作速度会比Old Generation区域的gc操作速度更快。

  1. Android应用的进程都是从一个叫做Zygote的进程fork出来的。Zygote进程在系统启动并且载入通用的framework的代码与资源之后开始启动。为了启动一个新的程序进程,系统会fork Zygote进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这使得大多数的RAM pages被用来分配给framework的代码,同时使得RAM资源能够在应用的所有进程之间进行共享。
  2. 大多数static的数据被mmapped到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code,app resources,so文件等。
  3. 大多数情况下,Android通过显式的分配共享内存区域(例如ashmem或者gralloc)来实现动态RAM区域能够在不同进程之间进行共享的机制。例如,Window Surface在App与Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider与Clients之间共享内存。
  1. 每一个进程的Dalvik heap都反映了使用内存的占用范围。这就是通常逻辑意义上提到的Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定的上限。
  2. 逻辑上讲的Heap Size和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及和其他进程进行共享的内存。
  1. 为了整个Android系统的内存控制需要,Android系统为每一个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OutOfMemoryError的错误。
  2. ActivityManager.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,表明你的应用的Heap Size阈值是多少Mb(megabates)。
  1. Android系统并不会在用户切换应用的时候做交换内存的操作。Android会把那些不包含Foreground组件的应用进程放到LRU Cache中。例如,当用户开始启动了一个应用,系统会为它创建了一个进程,但是当用户离开这个应用,此进程并不会立即被销毁,而是会被放到系统的Cache当中,如果用户后来再切换回到这个应用,此进程就能够被马上完整的恢复,从而实现应用的快速切换。
  2. 如果你的应用中有一个被缓存的进程,这个进程会占用一定的内存空间,它会对系统的整体性能有影响。因此当系统开始进入Low Memory的状态时,它会由系统根据LRU的规则与应用的优先级,内存占用情况以及其他因素的影响综合评估之后决定是否被杀掉。

需要特别注意的:

内存碎片的产生
如上图所示,第一行,在开始阶段,内存分配较满;第二行,经过GC之后,大部分对象被释放。此时可能产生的问题是,因为没有内存整理功能,整个页面的4KB内存(内存分配的最小单位是页面,通常为4KB)可能只有一个小对象,但是统计PrivateDirty/Pss时还是按照4KB计算。所以对于Dalvik虚拟机的手机来说,我们首先要尽量避免掉频繁生成很多临时小变量(比如说:getView, onDraw等函数中new对象),另一个又要尽量去避免产生很多长生命周期的大对象。

六、Android GC何时发生?

由上文我们知道,GC操作主要是由系统决定的,但是我们可以监听系统的GC过程,以此来分析我们应用程序当前的内存状态。
Dalvik虚拟机,每一次GC打印内容格式:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

含义解析

Art虚拟机,每一次GC打印内容格式:

I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>

基本情况和Dalvik没有什么差别,GC的Reason更多了,还多了一个LOS_Space_Status.

LOS_Space_Status:Large Object Space,大对象占用的空间,这部分内存并不是分配在堆上的,但仍属于应用程序内存空间,主要用来管理 Bitmap 等占内存大的对象,避免因分配大内存导致堆频繁 GC。

七、获取内存使用情况

通过命令行adb shell dumpsys meminfo packagename查看内存详细占用情况:

命令行查看内存分配情况

其中几个关键的数据:

参考:

欢迎关注微信公众号:定期分享Java、Android干货!

欢迎关注
上一篇 下一篇

猜你喜欢

热点阅读