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

一、初识内存优化

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

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

二、内存分配

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

JVM运行时数据区

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

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

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

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

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

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

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

三、内存回收

3.1标记-清除算法

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

  1. 效率问题,标记和清除两个过程的效率都不高;
  2. 空间问题,标记清除以后会产生大量的不连续的内存碎片。

“标记-清除”算法示意图

3.2复制算法

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

  • 优势:实现简单,运行高效;每次都是对整个半区进行内存回收,内存分配时也不须要考虑内存碎片等状况,只要移动堆顶指针,按顺序分配内存便可;
  • 缺点:粗暴的将内存缩小为原来的一半,代价实在有点高。

“复制”算法示意图

3.3标记-整理算法

先标记须要回收的对象(标记过程与“标记-清除”算法同样),而后把全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
这种方法的特色:

  • 避免了内存碎片;
  • 避免了“复制”算法50%的空间浪费;
  • 主要针对对象存活率高的老年代。

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

3.4分代收集算法

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

四、对象是否回收的依据

4.1引用计数算法

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

  • 优势:实现简单,断定效率也很高;
  • 缺点:难以解决对象之间相互循环引用致使计数器值不等于0的问题。

4.2可达性分析算法

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

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

五、Android的内存管理

Android系统的ART和Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色, 使用pagingmemory-mapping来管理内存,这意味着无论是由于建立对象仍是使用使用内存页面形成的任何被修改的内存,都会一直存在于内存中,App惟一释放内存的方法就是释放App持有的对象引用,使GC能够回收。

Android Runtime内存堆划分

  • 5.1内存回收

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

  • 5.2共享内存

    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之间共享内存。
  • 5.3分配与回收内存

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

    1. 为了整个Android系统的内存控制须要,Android系统为每个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不一样的设备上会由于RAM大小不一样而各有差别。若是你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引发OutOfMemoryError的错误。
    2. ActivityManager.getMemoryClass()能够用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,代表你的应用的Heap Size阈值是多少Mb(megabates)。
  • 5.5应用切换

    1. Android系统并不会在用户切换应用的时候作交换内存的操做。Android会把那些不包含Foreground组件的应用进程放到LRU Cache中。例如,当用户开始启动了一个应用,系统会为它建立了一个进程,可是当用户离开这个应用,此进程并不会当即被销毁,而是会被放到系统的Cache当中,若是用户后来再切换回到这个应用,此进程就可以被立刻完整的恢复,从而实现应用的快速切换。
    2. 若是你的应用中有一个被缓存的进程,这个进程会占用必定的内存空间,它会对系统的总体性能有影响。所以当系统开始进入Low Memory的状态时,它会由系统根据LRU的规则与应用的优先级,内存占用状况以及其余因素的影响综合评估以后决定是否被杀掉。

须要特别注意的:

  • 在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一个很好的能力。

6、Android 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操做致使应用程序暂停的时间。关于这个暂停的时间,在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_Status:Large Object Space,大对象占用的空间,这部份内存并非分配在堆上的,但仍属于应用程序内存空间,主要用来管理 Bitmap 等占内存大的对象,避免因分配大内存致使堆频繁 GC。

7、获取内存使用状况

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

命令行查看内存分配状况
其中几个关键的数据:

  • 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的内存泄露。

参考:

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

欢迎关注
相关文章
相关标签/搜索