首页 > 代码库 > Android 内存管理机制详解

Android 内存管理机制详解

??嵌入式设备的一个普遍特点是内存容量相对有限。当运行的程序超过一定数量时,或者涉及复杂的计算时,很可能出现内存不足,进而导致系统卡顿的现象。Android 系统也不例外,它同样面临着设备物理内存短缺的困境。对于已经启动过一次的Android程序,再一次启动所花的时间会明显减少。原因在于Android系统并不马上清理那些已经”淡出视野”的程序(比如你调用Activity.finish退出UI界面)。它们在一定的时间里仍然驻留在内存中。这样做的好处是明显的,即下一次启动不需要再为程序重新创建一个进程;坏处就是,加大了内存OOM的概率。

Linux内存监控机制(OOMKiller)

??Android是基于Linux的,而Linux底层内核有自己的内存监控机制,即OOMKiller。一旦发现系统的可用内存达到临界值,这个OOM的管理者就会自动跳出来清理内存。

OOMKiller有不同的策略和不同的处理手段。它的核心思想如下:

按照优先级顺序,从低到高逐步杀掉进程,回收内存。

优先级的设定策略主要考虑两个方面:一个是要考虑对系统的损害程度(例如系统的核心进程,优先级通常较高),另一方面也希望尽可能多地释放无用内存。一个合理的策略至少需要考虑以下几个因素:

  • 进程消耗的内存
  • 进程占用的CPU时间
  • oom_adj(OOM权重)

??内核所管理的进程都有一个衡量其oom权重的值,存储在/proc/< PID >/oom_adj中。根据这一权重值以及上面所提及的若干其他因素,系统会实时给每个进程评分,以决定OOM时应该杀死哪些进程。
这个值存储在/proc/< PID >/oom_score中。
oom_score分数越低的进程,被杀死的概率越小,或者说被杀死的时间越晚。
下面展示了PID为5912的NetworkManager进程的oom_adj 和oom_score,可以看到分数很低,说明此进程十分重要,一般不会被系统杀死。

技术分享

Android 内存管理机制

基于Linux内核OOM Killer的核心思想,Android 系统扩展出了自己的内存监控体系。因为Linux下的内存杀手需要等到系统资源”濒临绝境”的情况下才会产生效果,而Android则实现了自己的Killer.

Android 系统为此开发了一个专门的驱动,名为Low Memory Killer(LMK)。源码路径在内核工程的 drivers/staging/android/Lowmemorykiller.c中。
它的驱动加载函数如下:

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}

可见LMK向内核线程注册了一个shrinker的监听回调,实现体为lowmem_shrinker。当系统的空闲页面低于一定阈值时,这个回调就会被执行。

Lowmemorykiller.c 中定义了两个数组,分别如下:

static short lowmem_adj[6] = {
    0,
    1,
    6,
   12,
 };
static int lowmem_adj_size = 4;//下面的数值以此为单位(页大小)
static int lowmem_minfree[6] = {
    3 * 512,    /* 6MB */
    2 * 1024,   /* 8MB */
    4 * 1024,   /* 16MB */
    16 * 1024,  /* 64MB */
};

第一个数组lowmem_adj最多有6个元素,默认只定义了4个,它表示可用容量处于”某层级”时需要被处理的adj值;第二个数组则是对”层级”的描述。这样说可能不清楚,举个例子,lowmem_minfree 的第一个元素是3*512,3*512*lowmem_adj_size=6MB.意味着当可用内存小于6MB时,Killer需要清理adj的值为0(即lowmem_adj的第一个元素)以下的那些进程。其中adj的取值范围是-17~15,数字越小表示进程级别越高,通常只有0-15被使用。

下图是LWK机制的实现简图。

技术分享

这两个数组只是系统的预定义值,我们可以根据项目的实际需求来定制。

Android系统提供了相应的文件来供我们修改这两组值。

路径如下:

/sys/module/lowmemorykiller/parameters/adj
/sys/module/lowmemorykiller/parameters/minfree

可以在 init.rc(系统启动时,由init进程解析的第一个脚本)中加入如下语句。init.rc路径为/system/core/rootdir/init.rc

/sys/module/lowmemorykiller/parameters/adj  0,8
/sys/module/lowmemorykiller/parameters/minfree 1024,4096

Android进程分类

进程omm_adj的大小跟进程的类型以及进程被调度的次序有关。

在Android中,进程主要分为以下几个种类:

1. 前台进程(foreground)

??目前正在屏幕上显示的进程和一些系统进程。举例来说,Dialer,Storage,Google Search等系统进程就是前台进程;再举例来说,当你运行一个程序,如浏览器,当浏览器界面在前台显示时,浏览器属于前台进程(foreground),但一旦你按home回到主界面,浏览器就变成了后台程序(background)。我们最不希望终止的进程就是前台进程。

2. 可见进程(visible)

??可见进程是一些不再前台,但用户依然可见的进程,举个例来说:widget、输入法等,都属于visible。这部分进程虽然不在前台,但与我们的使用也密切相关,我们也不希望它们被终止(你肯定不希望时钟、天气,新闻等widget被终止,那它们将无法同步,你也不希望输入法被终止,否则你每次输入时都需要重新启动输入法)。

3. 桌面进程(home app)

??即launcher,保证在多任务切换之后,可以快速返回到home界面而不需重新加载launcher。

4. 次要服务(secondary server)

??目前正在运行的一些服务(主要服务,如拨号等,是不可能被进程管理终止的,故这里只谈次要服务),举例来说:谷歌企业套件,Gmail内部存储,联系人内部存储等。这部分服务虽然属于次要服务,但很一些系统功能依然息息相关,我们时常需要用到它们,所以也不太希望他们被终止。

5. 后台进程(hidden)

??即是后台进程(background),就是我们通常意义上理解的启动后被切换到后台的进程,如浏览器,阅读器等。当程序显示在屏幕上时,他所运行的进程即为前台进程(foreground),一旦我们按home返回主界面(注意是按home,不是按back),程序就驻留在后台,成为后台进程(background)。后台进程的管理策略有多种:有较为积极的方式,一旦程序到达后台立即终止,这种方式会提高程序的运行速度,但无法加速程序的再次启动;也有较消极的方式,尽可能多的保留后台程序,虽然可能会影响到单个程序的运行速度,但在再次启动已启动的程序时,速度会有所提升。这里就需要用户根据自己的使用习惯找到一个平衡点。

6. 内容供应节点(content provider)

没有程序实体,进提供内容供别的程序去用的,比如日历供应节点,邮件供应节点等。在终止进程时,这类程序应该有较高的优先权。

7. 空进程(empty)

??没有任何东西在内运行的进程,有些程序,比如BTE,在程序退出后,依然会在进程中驻留一个空进程,这个进程里没有任何数据在运行,作用往往是提高该程序下次的启动速度或者记录程序的一些历史信息。这部分进程无疑是应该最先终止的

在AMS 用于处理进程的相关代码文件ProcessList.java 中,定义了不同类型的adj值,如下所示:

/**
*省略其它代码
*/
    //未知的adj
    static final int UNKNOWN_ADJ = 16;
    static final int CACHED_APP_MAX_ADJ = 15;
    static final int CACHED_APP_MIN_ADJ = 9;
    //B list of service ,和A list相比,对用户黏合度小一些
    static final int SERVICE_B_ADJ = 8;
    //用户前一次交互的进程
    static final int PREVIOUS_APP_ADJ = 7;
    //Launcher进程
    static final int HOME_APP_ADJ = 6;
    //运行了application service的进程
    static final int SERVICE_ADJ = 5;
    //重量级应用程序进程
    static final int HEAVY_WEIGHT_APP_ADJ = 4;
    //用于承载backup相关操作的进程
    static final int BACKUP_APP_ADJ = 3;
    //这类进程能被用户感觉到但不可见,如后台运行的音乐播放器
    static final int PERCEPTIBLE_APP_ADJ = 2;
    //有前台可见的Activity进程,如果轻易杀死这类进程,将严重影响用户体验
    static final int VISIBLE_APP_ADJ = 1;
    //当前正在运行的那个进程,即用户正在交互的程序
    static final int FOREGROUND_APP_ADJ = 0;
    static final int PERSISTENT_SERVICE_ADJ = -11;
    //persistent性质的进程,如telephony
    static final int PERSISTENT_PROC_ADJ = -12;
    //系统进程
    static final int SYSTEM_ADJ = -16;

/**
*省略其它代码
*/

上面定义的adj数值来看,adj越小表示进程类型就越重要,特别的,系统进程的默认oom_adj 为-16,这类进程永远不会被杀死。

还需要注意的是updateOomLevels函数,内部原理是通过写上面两个文件来实现,AMS 会根据系统的当前配置自动修正adj和minfree,以尽可能适配不同的硬件。函数源码如下所示:

private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
        // Scale buckets from avail memory: at 300MB we use the lowest values to
        // 700MB or more for the top values.
        float scaleMem = ((float)(mTotalMemMb-300))/(700-300);

        // Scale buckets from screen size.
        int minSize = 480*800;  //  384000
        int maxSize = 1280*800; // 1024000  230400 870400  .264
        float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
        if (false) {
            Slog.i("XXXXXX", "scaleMem=" + scaleMem);
            Slog.i("XXXXXX", "scaleDisp=" + scaleDisp + " dw=" + displayWidth
                    + " dh=" + displayHeight);
        }

        float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
        if (scale < 0) scale = 0;
        else if (scale > 1) scale = 1;
        int minfree_adj = Resources.getSystem().getInteger(
                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
        int minfree_abs = Resources.getSystem().getInteger(
                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);
        if (false) {
            Slog.i("XXXXXX", "minfree_adj=" + minfree_adj + " minfree_abs=" + minfree_abs);
        }

        // We‘ve now baked in the increase to the basic oom values above, since
        // they seem to be useful more generally for devices that are tight on
        // memory than just for 64 bit.  This should probably have some more
        // tuning done, so not deleting it quite yet...
        final boolean is64bit = false; //Build.SUPPORTED_64_BIT_ABIS.length > 0;

        for (int i=0; i<mOomAdj.length; i++) {
            int low = mOomMinFreeLow[i];
            int high = mOomMinFreeHigh[i];
            mOomMinFree[i] = (int)(low + ((high-low)*scale));
            if (is64bit) {
                // On 64 bit devices, we consume more baseline RAM, because 64 bit is cool!
                // To avoid being all pagey and stuff, scale up the memory levels to
                // give us some breathing room.
                mOomMinFree[i] = (3*mOomMinFree[i])/2;
            }
        }

        if (minfree_abs >= 0) {
            for (int i=0; i<mOomAdj.length; i++) {
                mOomMinFree[i] = (int)((float)minfree_abs * mOomMinFree[i]
                        / mOomMinFree[mOomAdj.length - 1]);
            }
        }

        if (minfree_adj != 0) {
            for (int i=0; i<mOomAdj.length; i++) {
                mOomMinFree[i] += (int)((float)minfree_adj * mOomMinFree[i]
                        / mOomMinFree[mOomAdj.length - 1]);
                if (mOomMinFree[i] < 0) {
                    mOomMinFree[i] = 0;
                }
            }
        }

        // The maximum size we will restore a process from cached to background, when under
        // memory duress, is 1/3 the size we have reserved for kernel caches and other overhead
        // before killing background processes.
        mCachedRestoreLevel = (getMemLevel(ProcessList.CACHED_APP_MAX_ADJ)/1024) / 3;

        // Ask the kernel to try to keep enough memory free to allocate 3 full
        // screen 32bpp buffers without entering direct reclaim.
        int reserve = displayWidth * displayHeight * 4 * 3 / 1024;
        int reserve_adj = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAdjust);
        int reserve_abs = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAbsolute);

        if (reserve_abs >= 0) {
            reserve = reserve_abs;
        }

        if (reserve_adj != 0) {
            reserve += reserve_adj;
            if (reserve < 0) {
                reserve = 0;
            }
        }

        if (write) {
            ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
            buf.putInt(LMK_TARGET);
            for (int i=0; i<mOomAdj.length; i++) {
                buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);
                buf.putInt(mOomAdj[i]);
            }

            writeLmkd(buf);
            SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
        }
        // GB: 2048,3072,4096,6144,7168,8192
        // HC: 8192,10240,12288,14336,16384,20480
    }

改变进程权重adj值

除了系统的评定标准,我们也可以用自己的方式来改变进程的权重值。

1.写文件

??和上述提到的adj和minfree类似,进程的oom_adj也可以通过写文件的形式来修改,路径为/proc/< PID >/oom_adj。比如init.rc中有如下语句

on early-init
 # Set init and its forked children‘s oom_adj.
 write /proc/1/oom_adj -16

PID 为1的进程为init程序,将此进程的oom_adj 设置为-16,保证它不会被杀死。

2.设置android:persistent

??对某些非常重要的程序,不希望它被系统杀死。在AndroidMainifest.xml文件中给application 标签添加”android:persistent=true”属性,将应用程序设置为常驻内存,但需要特别注意,如果应用程序本不够完善,而系统又不能正常回收,那么会导致意想不到的问题。


参考资料

《深入理解Android内核设计思想》

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android 内存管理机制详解