Android内存管理机制

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

Android内存管理机制_第1张图片

前言

Android 是基于 Linux 内核实现的操作系统,使用java语言进行开发,所以在了解Android内存管理机制之前,我们需要对Linux及java的内存管理及分配的相关知识有一个了解

Linux内存管理哲学

Linux 的内存管理哲学是:Free memory is wasted memory,即内存没得到充分利用就是在浪费资源

Android 对内存的使用方式同样是“尽最大限度的使用”,这一点继承了 Linux 的优点。只不过有所不同的是,Linux 侧重于尽可能多的使用内存缓存磁盘数据以降低磁盘 IO 进而提高系统的数据访问性能,而 Android 侧重于尽可能多的缓存进程以提高应用启动和切换速度。Linux 系统在进程活动停止后就结束该进程,而 Android 系统则会在内存中尽量长时间的保持应用进程,直到系统需要更多内存为止

Java内存区域分配

Android基于java进行开发,所以同时遵循java的内存分配规则

Android内存管理机制_第2张图片
android内存区域划分
  • 程序计数器(program counter register):程序计数器是一个比较校的内存单元,用来表示当前程序运行哪里的一个指示器。由于每个线程都由自己的执行顺序,所以程序计数器是线程私有的,每个线程都要由一个自己的程序计数器来指示自己(线程)下一步要执行哪条指令

如果程序执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地方法(native方法),那么计数器的值为Undefined。由于程序计数器记录的只是当前指令地址,所以不存在内存泄漏的情况,也是jvm内存区域中唯一一个没有OOM定义的区域

  • 虚拟机栈(JVM stack):当线程的每个方法在执行的时候都会创建一个栈帧(Stack Frame)用来存储方法中的局部变量、方法出口等,同时会将这个栈帧放入JVM栈中,方法调用完成时,这个栈帧出栈。每个线程都要一个自己的虚拟机栈来保存自己的方法调用时候的数据,因此虚拟机栈也是线程私有的

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,抛出StackOverFlowError,不过虚拟机基本上都允许动态扩展虚拟机栈的大小。这样的话线程可以一直申请栈,直到内存不足的时候,会抛出OOM

  • 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈类似,只是本地方法栈存放的栈帧是在native方法调用的时候产生的。有的虚拟机中会将本地方法栈和虚拟栈放在一起,因此本地方法栈也是线程私有的
  • 堆(Heap):堆是java GC机制中最重要的区域。堆是为了放置“对象的实例”,对象都是在堆区上分配内存的,堆在逻辑上连续,在物理上不一定连续。所有的线程共用一个堆,堆的大小是可扩展的,如果在执行GC之后,仍没有足够的内存可以分配且堆大小不可再扩展,将会抛出OOM
  • 方法区(Method Area):又叫静态区,用于存储类的信息、常量池等,逻辑上是堆的一部分,是各个线程共享的区域,为了与堆区分,又叫非堆。在永久代还存在时,方法区被用作永久代。方法区可以选择是否开启垃圾回收。jvm内存不足时会抛出OOM
  • 直接内存(Direct Memory):直接内存指的是非jvm管理的内存,是机器剩余的内存。用基于通道(Channel)和缓冲区(Buffer)的方式来进行内存分配,用存储在JVM中的DirectByteBuffer来引用,当机器本身内存不足时,也会抛出OOM

Java内存回收算法

标记-清除算法

步骤

  • 标记出所有需要回收的对象
  • 统一回收所有被标记的的对象

内存整理前

Android内存管理机制_第3张图片

内存整理后

Android内存管理机制_第4张图片

特点

  • 标记清除效率不高
  • 产生大量不连续的内存碎片

复制算法

步骤

  • 将内存划分为两块
  • 一块内存用完后复制存活对象到另一块
  • 清理另一块内存

内存整理前

Android内存管理机制_第5张图片

内存整理后

Android内存管理机制_第6张图片

特点

  • 实现简单,运行高效(相对于标记-清除算法,因为每次只需要对内存的1/2进行标记)
  • 浪费一半空间,代价大

标记-整理算法

步骤

  • 标记过程与标记-清除算法一样
  • 存活对象往一端移动
  • 清理剩余内存

内存整理前

Android内存管理机制_第7张图片

内存整理后

Android内存管理机制_第8张图片

特点

  • 避免标记-清理所导致的内存碎片
  • 避免复制算法的空间浪费

分代回收算法

android采用分代回收算法,最近分配的对象属于年轻代(Young Generation)。 当一个对象长时间保持活动状态时,它可以被提升为年老代(Older Generation),之后还能进一步提升为持久代(Permanent Generation

Android内存管理机制_第9张图片
  • Young Generation(年轻代):Faster but more frequent,垃圾回收效率高且频繁

年轻代分为三个区,一个 eden 区,另外的两个 S0 和 S1 都是 Survivor 区(S0S1 只是为了说明,两者实质上一样,方向可互换),内存大小edenS0S1 = 8:1:1。程序中生成的大部分新的对象都在 eden 区中,当 eden 区满时,还存活的对象将被复制到其中一个 Survivor 区,当此 Survivor 区的对象占用空间满时,此区存活的对象又被复制到另外一个 Survivor 区,当这个 Survivor 区也满时,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制到年老代

年轻代垃圾回收频繁但是最终留存下来较少,所有年轻代使用 "标记-复制" 法进行 GC

  • Old Generation(年老代):Slower but less frequent,垃圾回收速度慢且相对年轻代频率较低

年老代存放的是上面年轻代复制过来的对象,也就是在年轻代中还存活的对象,并且区满了复制过来的。一般来说,年老代中的对象生命周期都比较长

老年代相对稳定但存活的对象较多,所以老年代使用 "标记-整理" 法进行 GC

  • Permanent Generation(永久代)
    JDK 1.8之后已经取消永久代
    用于存放静态的类和方法,垃圾回收对持久代没有显著影响

在三级 Generation 内存模型中,每一个区域的大小都是有固定值的,当进入的对象总大小到达某一级内存区域阀值的时候就会触发 GC 机制,进行垃圾回收,腾出空间以便其他对象进入

java GC机制工作原理

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。
以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况

public class Test {
    public static void main(String a[]) {
        Object o1 = new Object();
        Object o2 = new Object();
        o2 = o1;
    }
}
Android内存管理机制_第10张图片

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高
对于程序员来说,GC基本是不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性

Android内存阈值

为了整个Android系统的内存控制需要,Android系统为每个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上回因为RAM大小不同而有所差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OutOfMemoryError的错误。我们可以通过getMemoryClass()方法查看当前应用程序的内存阈值

// 获取Heap Size阈值
ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
// 返回值是以Mb为单位
int memoryClass = am.getMemoryClass();

所以,只要allocated + 新分配的内存 >= getMemoryClass()的时候就会发生OOM。而不是需要等到整个设备的RAM都满了才会发生OOM

Android进程管理策略

前面我们说了

Linux 系统在进程活动停止后就结束该进程,而 Android 系统则会在内存中尽量长时间的保持应用进程,直到系统需要更多内存为止

那么我们就要了解android有哪些进程类型,他们的销毁策略及优先级是如何的?

Android进程类型

  • 前台进程(foreground process,正常不会被杀死)

前台进程是用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

  1. 托管用户正在交互的 Activity(已调用 ActivityonResume() 方法)
  2. 托管某个 Service,后者绑定到用户正在交互的 Activity
  3. 托管正执行一个生命周期回调的 ServiceonCreate()onStart()onDestroy()
  4. 托管正执行其 onReceive() 方法的 BroadcastReceiver

通常,系统中只会有少量几个前台进程的存在,前台进程是最高级的进程,只有在系统内存极其不足,甚至系统剩余内存都不足以让这些前台进程正常运行的情况下,系统才会杀死他们,回收内存空间。在这种情况下,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

  • 可见进程(visible process,正常不会被杀死)

可见进程是没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程,杀死这类进程也会明显影响用户体验。 如果一个进程满足以下任一条件,即视为可见进程:

  1. 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,启动了一个对话框样式的前台 activity ,此时在其后面仍然可以看到前一个Activity。(运行时权限对话框就属于此类。考虑一下,还有哪种情况会导致只触发onPause()而不触发onStop()?)
  2. 托管通过 Service.startForeground() 启动的前台Service。(Service.startForeground():它要求系统将它视为用户可察觉到的服务,或者基本上对用户是可见的。)
  3. 托管系统用于某个用户可察觉的特定功能的Service,比如动态壁纸、输入法服务等等。

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。如果这类进程被杀死,从用户的角度看,这意味着当前 activity 背后的可见 activity 会被黑屏代替。

  • 服务进程(service process,正常不会被杀死)

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,后台网络上传或下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

  • 后台进程(Background / Cached Process,随时被杀死)

这类进程一般会持有一个或多个目前对用户不可见的 Activity (已调用 ActivityonStop() 方法)。它们不是当前所必须的,因此当其他更高优先级的进程需要内存时,系统可能随时终止它们以回收内存。但如果正确实现了Activity的生命周期,即便系统终止了进程,当用户再次返回应用时也不会影响用户体验:关联Activity在新的进程中被重新创建时可以恢复之前保存的状态。

  • 空进程(Empty Process,随时被杀死)

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

进程销毁策略(ADJ)

从上面的介绍中我们知道了,android的进程有很多种类型。假设我们选择在内存不足的时候杀死进程,那么一个很自然的问题就浮现出来:到底干掉哪一个呢?Linux内核的算法倒是非常简单,那就是打分(oom_score),找到分数最高的就OK了。那么怎么来算分数呢?可以参考内核中的oom_badness函数:

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
              const nodemask_t *nodemask, unsigned long totalpages)
{
    long points;
    long adj;

    if (oom_unkillable_task(p, memcg, nodemask))
        //如果进程不可被杀,直接跳过
        return 0;

    p = find_lock_task_mm(p);
    if (!p)
        return 0;

    /*
     * Do not even consider tasks which are explicitly marked oom
     * unkillable or have been already oom reaped or the are in
     * the middle of vfork
     */
    //获取当前进程的oom_score_adj参数
    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN ||
            test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
            in_vfork(p)) {
        task_unlock(p);
      //如果当前进程oom_score_adj为OOM_SCORE_ADJ_MIN的话,就返回0.等于告诉OOM,此进程不参数'bad'评比
        return 0;
    }

    /*
     * The baseline for the badness score is the proportion of RAM that each
     * task's rss, pagetable and swap space use.
     */
   //可以看出points综合了内存占用情况,包括RSS部分、swap file或者swap device占用内存、以及页表占用内存
    points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
        atomic_long_read(&p->mm->nr_ptes) + mm_nr_pmds(p->mm);
    task_unlock(p);

    /*
     * Root processes get 3% bonus, just like the __vm_enough_memory()
     * implementation used by LSMs.
     */
    if (has_capability_noaudit(p, CAP_SYS_ADMIN))
        //如果是root用户,增加3%的使用特权
        points -= (points * 3) / 100;

    /* Normalize to oom_score_adj units */
    //这里可以看出oom_score_adj对最终分数的影响
    //如果oom_score_adj小于0,则最终points就会变小,进程更加不会被选中。
    adj *= totalpages / 1000;
      //将归一化后的adj和points求和,作为当前进程的分数
    points += adj;

    /*
     * Never return 0 for an eligible task regardless of the root bonus and
     * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
     */
    return points > 0 ? points : 1;
}

  • oom_score_adj是影响最终分数的一个比较重要的参数,oom_score_adj的取值范围是-1000~1000
  • 进程的 oom_score_adj 越大,表示此进程优先级越低,越容易被杀死回收;反之,越小,表示进程优先级越高,越不容易被杀死回收
  • 进程的 oom_score_adj 的值不是一成不变的,Android 系统会根据应用是否在前台等情况改变该值
  • 普通app进程的 oom_score_adj >= 0,系统进程的 oom_score_adj 才可能 < 0

其实下面的预定义值也是对应用进程的一种分类,它们是:

ADJ级别 取值 含义
NATIVE_ADJ -1000 native进程
SYSTEM_ADJ -900 仅指system_server进程
PERSISTENT_PROC_ADJ -800 系统persistent进程
PERSISTENT_SERVICE_ADJ -700 关联着系统或persistent进程
FOREGROUND_APP_ADJ 0 前台进程
VISIBLE_APP_ADJ 100 可见进程
PERCEPTIBLE_APP_ADJ 200 可感知进程,比如后台音乐播放
BACKUP_APP_ADJ 300 备份进程
HEAVY_WEIGHT_APP_ADJ 400 重量级进程
SERVICE_ADJ 500 服务进程
HOME_APP_ADJ 600 Home进程
PREVIOUS_APP_ADJ 700 上一个进程
SERVICE_B_ADJ 800 B List中的Service
CACHED_APP_MIN_ADJ 900 不可见进程的adj最小值
CACHED_APP_MAX_ADJ 906 不可见进程的adj最大值

那么系统什么时候更新 oom_score_adj

系统会对处于不同状态的进程设置不同的优先级。但实际上,进程的状态是一直在变化中的。例如:用户可以随时会启动一个新的 Activity,或者将一个前台的 Activity 切换到后台。在这个时候,发生状态变化的 Activity 的所在进程的优先级就需要进行更新

ActivityManagerService 中有如下两个方法用来更新进程的优先级:

  • final boolean updateOomAdjLocked(ProcessRecord app)
  • final void updateOomAdjLocked()

第一个方法是针对指定的单个进程更新优先级。第二个是对所有进程更新优先级

下面以一幅图来诠释整个ADJ算法的精髓,几乎涵盖了oom_score_adj更新的绝大多数场景

Android内存管理机制_第11张图片

系统什么时候根据 oom_score_adj 杀进程?

ActivityManagerService 调用updateOomAdjLocked()时,会判断进程是否需要被杀死,若是,则调用ProceeRecord::kill()方法杀死该进程

一些建议

  1. UI进程与Service进程一定要分离,因为对于包含ActivityService进程,一旦进入后台就成为”cch-started-ui-services”类型的cache进程(ADJ>=900),随时可能会被系统回收;而分离后的Service进程服务属于SERVICE_ADJ(500),被杀的可能性相对较小。尤其是系统允许自启动的服务进程必须做UI分离,避免消耗系统较大内存
  2. 只有真正需要用户可感知的应用,才调用startForegroundService()方法来启动前台服务,此时ADJ=PERCEPTIBLE_APP_ADJ(200),常驻内存,并且会在通知栏常驻通知提醒用户,比如音乐播放,地图导航。切勿为了常驻而滥用前台服务,这会严重影响用户体验
  3. 进程中的Service工作完成后,务必主动调用stopServicestopSelf来停止服务,避免占据内存,浪费系统资源
  4. 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存
  5. APP应该实现接口onTrimMemory()onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放。当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次
  6. 减少在保活上花心思,更应该在优化内存上下功夫,因为在相同ADJ级别的情况下,系统会选择优先杀内存占用的进程

总结

本文我们主要讲了android基于linux系统,及java编程语言进行开发,继承了一些比较好的的内存管理策略,但同时又有一些独特平台特性。计算机行业就是这样,一个成功的技术往往是”站在巨人的肩膀上“,取其精华,去其糟粕,相互融合与碰撞,从而得出一个最优解

你可能感兴趣的:(Android内存管理机制)