Android性能优化之内存管理

Android性能优化之内存管理

一、内存分配

说到Android的内存分配,就不得不提Java中的内存管理。Java程序在运行时将数据划分为若干不同数据区: 方法区(线程共享)、堆区-heap区(线程共享)、虚拟机栈(线程私有)、本地方法栈(线程私有)、程序计数器(线程私有)。

  • 1、方法区: 存放类信息、常量、静态变量,线程共享区域

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

  • 3、本地方法栈: 与虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务的,本地方法栈是为虚拟机执行使用到的Native方法服务的,线程私有区域。

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

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

二、内存回收

1、标记-清除算法

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

优点:

  • 实现简单

缺点:

  • 效率问题,标记和清除两个过程的效率都不高;

  • 空间问题,标记清除之后会产生大量的不连续的内存碎片。

2、复制算法

将内存按容量划分大小相等的两部分,每次只使用其中一半,当这半内存用完了,就将存活的对象复制到另一半内存上,然后再把这一半全部一次性清理。

优点:

  • 实现较简单、运行高效;

  • 没有内存碎片的问题;因为每次都是一次性清理一半内存,所以不存在内存碎片问题,只要一动堆顶指针,按顺序分配内存即可。

缺点:

  • 由于将每次都将内存分为两半,只有一半被使用,内存使用代价高且内存利用率不高

3、标记-整理算法

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

优点:

  • 避免了内存碎片的问题

  • 避免了复制算法50%内存空间的浪费

  • 主要应用于存活率比较高的老年代

缺点:

  • 运行效率相对较低, 因为需要将存活对象指针向一端移动。

4、分代收集算法

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

四、对象是否已死

如何判断一个对象是否具备回收的条件

1、引用计数法

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

  • 优点: 实现简单,效率比较高

  • 缺点: 无法解决对象之间相互引用导致计数器的值不为0的问题

2、可达性分析算法

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

五、Android的内存管理

Android系统中的ART和Dalvik虚拟机起到了常规的内存垃圾自动回收的作用,使用Paging(分页)和内存映射来管理内存,这意味着不管是创建对象还是使用内存造成内存被修改的内存都会一直存在于内存中,App唯一释放内存的方法就是释放App持有的对象引用,使得GC可以将其回收。

1、内存回收

在Android系统高版本中针对Heap堆区有一个堆区内存分代模型,比如最近分配的对象会存放在新生代区,当这个对象在新生代存活时间达到一定程度,就会被移动到老年代区,最后累积到一定时间就再被移动到永久代区。 系统根据内存中不同的内存数据类型分别执行不同GC操作,例如刚分配到新生代去的对象更容易被销毁回收,同时新生代区的GC速度会比老年代区的GC速度更快。

2、共享内存

  • Android应用的进程都是从一个叫做Zygote的进程fork出来的。Zygote进程在系统启动并且载入通用的framework的代码与资源之后开始启动。为了启动一个新的程序进程,系统会fork Zygote进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这使得大多数的RAM pages被用来分配给framework的代码,同时使得RAM资源能够在应用的所有进程之间进行共享。
  • 大多数static的数据被mmapped到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code,app resources,so文件等。
  • 大多数情况下,Android通过显式的分配共享内存区域(例如ashmem或者gralloc)来实现动态RAM区域能够在不同进程之间进行共享的机制。例如,Window Surface在App与Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider与Clients之间共享内存。

3、分配与回收内存

  • 每一个进程的Dalvik heap都反映了使用内存的占用范围。这就是通常逻辑意义上提到的Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定的上限。
  • 逻辑上讲的Heap Size和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及和其他进程进行共享的内存。

4、限制应用的内存

  • 为了整个Android系统的内存控制需要,Android系统为每一个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OutOfMemoryError的错误。
  • ActivityManager.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,表明你的应用的Heap Size阈值是多少Mb(megabates)。

5、应用切换

  • Android系统并不会在用户切换应用的时候做交换内存的操作。Android会把那些不包含Foreground组件的应用进程放到LRU Cache中。例如,当用户开始启动了一个应用,系统会为它创建了一个进程,但是当用户离开这个应用,此进程并不会立即被销毁,而是会被放到系统的Cache当中,如果用户后来再切换回到这个应用,此进程就能够被马上完整的恢复,从而实现应用的快速切换。

  • 如果你的应用中有一个被缓存的进程,这个进程会占用一定的内存空间,它会对系统的整体性能有影响。因此当系统开始进入Low Memory的状态时,它会由系统根据LRU的规则与应用的优先级,内存占用情况以及其他因素的影响综合评估之后决定是否被杀掉。

6、注意

  • 在Dalvik下,大部分Davik采取的都是标记-清理回收算法,而且具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。标记-清理回收算法无法对Heap中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间;这样内存空洞就产生了。

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

  • ART在GC上不像Dalvik仅有一种回收算法,ART在不同的情况下会选择不同的回收算法。应用程序在前台运行时,响应性是最重要的,因此也要求执行的GC是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC适合作为Foreground GC,而Mark-Compact GC适合作为Background GC。 由于有Compact的能力存在,内存碎片在ART上可以很好的被避免,这个也是ART一个很好的能力。

六、GC触发时机

GC操作主要是由系统决定的,但是我们可以监听系统的GC过程,以此来分析我们应用程序当前的内存状态。

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

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

含义解析

  • GC Reason:GC触发原因
    GC_CONCURRENT:当已分配内存达到某一值时,触发并发GC;
    GC_FOR_MALLOC:当尝试在堆上分配内存不足时触发的GC;系统必须停止应用程序并回收内存;
    GC_HPROF_DUMP_HEAP: 当需要创建HPROF文件来分析堆内存时触发的GC;
    GC_EXPLICIT:当明确的调用GC时,例如调用System.gc()或者通过DDMS工具显式地告诉系统进行GC操作等;
    GC_EXTERNAL_ALLOC: 仅在API级别为10或者更低时(新版本分配内存都在Dalvik堆上)
  • Amount freed GC:回收的内存大小
  • Heap stats:堆上的空闲内存百分比 (已用内存)/(堆上总内存)
  • External memory stats: API级别为10或者更低:(已分配的内存量)/ (即将发生垃圾的极限)
  • Pause time(GC停顿时间):这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间。

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_StatusLarge Object Space,大对象占用的空间,这部分内存并不是分配在堆上的,但仍属于应用程序内存空间,主要用来管理 Bitmap 等占内存大的对象,避免因分配大内存导致堆频繁 GC

七、获取内存使用情况

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

loading-ag-615

其中几个关键的数据:

  • Private(Clean和Dirty的): 应用进程单独使用的内存,代表着系统杀死你的进程后可以实际回收的内存总量。通常需要特别关注其中更为昂贵的dirty部分,它不仅只被你的进程使用而且会持续占用内存而不能被从内存中置换出存储。申请的全部Dalvik和本地heap内存都是Dirty的,和Zygote共享的Dalvik和本地heap内存也都是Dirty的。
  • Dalvik Heap: Dalvik虚拟机使用的内存,包含dalvik-heap和dalvik-zygote,堆内存,所有的Java对象实例都放在这里。
  • Heap Alloc: 累加了Dalvik和Native的heap。
  • PSS: 这是加入与其他进程共享的分页内存后你的应用占用的内存量,你的进程单独使用的全部内存也会加入这个值里,多进程共享的内存按照共享比例添加到PSS值中。如一个内存分页被两个进程共享,每个进程的PSS值会包括此内存分页大小的一半在内。
    Dalvik Pss内存 = 私有内存Private Dirty + (共享内存Shared Dirty / 共享进程数)
  • TOTAL: 上面全部条目的累加值,全局的展示了你的进程占用的内存情况。
  • ViewRootImpl: 应用进程里的活动窗口视图个数,可以用来监测对话框或者其他窗口的内存泄露。
  • AppContexts及Activities: 应用进程里Context和Activity的对象个数,可以用来监测Activity的内存泄露。

   转载规则


《Android性能优化之内存管理》 mikyou 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Android线程池ThreadPoolExecutor源码解析 Android线程池ThreadPoolExecutor源码解析
Android线程池ThreadPoolExecutor源码解析一、ThreadPoolExecutor介绍1、定义 ThreadPoolExecutor线程池本质就是缓存一定线程数量的区域(池子) 2、作用 实现对批量线程的统一管理、
2019-12-28
下一篇 
UI组件化方案的思考 UI组件化方案的思考
UI组件化方案的思考一、 背景 平时在开发大量业务的时候,是否经常会有这样感受: 这个页面貌似以前写过,这块UI界面和我之前写的太像了吧。你会发现很多业务UI界面都是很类似,而且有些甚至一样,但是这些UI页面逻辑确实很像,可是它们都来自不同
2019-12-25