1、初识内存优化

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

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

2、内存分配

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




android 堆内存设置 android内存管理机制_Android


   


    方法区:方法区是被线程共享的。用于存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息)、静态变量、常量、即时编译器编译后的代码等数据。

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

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

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

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


备注:

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

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

3、内存回收

3.1标记-清除算法

3.2复制算法

3.3标记-整理算法

3.4分代收集算法

4、对象是否回收的依据

4.1引用计数算法

4.2可达性分析算法

Java虚拟机内存管理(二)--垃圾收集器及内存分配策略

5、Android的内存管理

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

内存回收

    在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。

应用切换

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

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

需要特别注意的是:

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

android 堆内存设置 android内存管理机制_android 堆内存设置_02


    如上图所示,第一行,在开始阶段,内存分配较满;第二行,经过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一个很好的能力。

六、Android GC何时发生?

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

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


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



解释如下:

    1. 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堆上)

    2. Amount freed GC:回收的内存大小

    3. Heap stats:堆上的空闲内存百分比 (已用内存)/(堆上总内存)

    4. External memory stats: API级别为10或者更低:(已分配的内存量)/ (即将发生垃圾的极限)

    5. 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。

七、获取内存使用情况

    通过命令行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的内存泄露。

内存泄露

    什么是内存泄露?内存泄露发生的场景有哪些?

    当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄露。

    内存泄露的场景有很多。

  • 非静态内部类的静态实例
  • 类的静态变量持有大数据对象
  • 资源对象未关闭
  • 注册对象未反注册
  • Handler临时性内存泄漏

可参考:Android内存优化--OOM