Android LowMemoryKiller 簡介

Yuloran發表於2019-01-01

讀書筆記,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)

前言

筆者在之前的文章《分析並優化 Android 應用記憶體佔用》中提到,為了避免 Cached Pages 太少時導致裝置卡頓、當機、重啟等情況,Android 引入了 LowMemoryKiller(源自 Linux OOM Killer) 機制,提前回收優先順序比較低的程式所佔的資源,以保證一個較好的使用者體驗。程式優先順序列表由 SystemServer 程式維護:

Android LowMemoryKiller  簡介

其中 Cached 程式列表,使用的是 LRU 演算法。

每個 Java 程式都有一個相關聯的 ProcessRecord 物件,其成員變數 curAdj 就表示該程式當前狀態下的優先順序:

    int curAdj; // Current OOM adjustment for this process
複製程式碼

當裝置可用記憶體低於 LMK 閾值時,LMK 便會根據程式的優先順序,逐級殺死程式並釋放其佔用的資源。

建議先閱讀筆者前四篇博文,以對 Android 記憶體相關有一個比較全面而深入的瞭解:

本篇博文為 Android 記憶體系列的最後一篇。

程式的生命週期

本小節摘自 Google Android Developers 《程式和執行緒》,LMK 殺死程式時,將遵從 空程式 -> 後臺程式 -> 服務程式 -> 可見程式 -> 前臺程式 的順序:

前臺程式

使用者當前操作所必需的程式。如果一個程式滿足以下任一條件,即視為前臺程式:

  • 託管使用者正在互動的 Activity(已呼叫 Activity 的 onResume() 方法)
  • 託管某個 Service,該 Service 繫結到使用者正在互動的 Activity
  • 託管正在“前臺”執行的 Service(服務已呼叫 startForeground())
  • 託管正執行一個生命週期回撥的 Service(onCreate()、onStart() 或 onDestroy())
  • 託管正執行其 onReceive() 方法的 BroadcastReceiver

通常,在任意給定時間前臺程式都為數不多。只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會終止它們。

可見程式

沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程式。 如果一個程式滿足以下任一條件,即視為可見程式:

  • 託管不在前臺、但仍對使用者可見的 Activity(已呼叫其 onPause() 方法)。例如,如果前臺 Activity 啟動了一個對話方塊,則有可能會發生這種情況。
  • 託管繫結到可見(或前臺)Activity 的 Service。

可見程式被視為是極其重要的程式,除非為了維持所有前臺程式同時執行而必須終止,否則系統不會終止這些程式。

服務程式

正在執行已使用 startService() 方法啟動的服務且不屬於上述兩個更高類別程式的程式。

儘管服務程式與使用者所見內容沒有直接關聯,但是它們通常在執行一些使用者關心的操作(例如,在後臺播放音樂或從網路下載資料)。因此,除非記憶體不足以維持所有前臺程式和可見程式同時執行,否則系統會讓服務程式保持執行狀態。

後臺程式

包含目前對使用者不可見的 Activity 的程式(已呼叫 Activity 的 onStop() 方法)。

這些程式對使用者體驗沒有直接影響,系統可能隨時終止它們,以回收記憶體供前臺程式、可見程式或服務程式使用。 通常會有很多後臺程式在執行,因此它們會儲存在 LRU (最近最少使用)列表中,以確保包含使用者最近檢視的 Activity 的程式最後一個被終止。

空程式

不含任何活動應用元件的程式。

保留這種程式的的唯一目的是用作快取,以縮短下次在其中執行元件所需的啟動時間。 為使總體系統資源在程式快取和底層核心快取之間保持平衡,系統往往會終止這些程式。

LowMemoryKiller

Andorid 的 Low Memory Killer 是在標準的 Linux Kernel 的 OOM Killer 基礎上修改而來的一種記憶體管理機制。當系統記憶體不足時,殺死不重要的程式以釋放其記憶體。LMK 的關鍵引數有 3 個:

  • oom_adj:在 Framework 層使用,代表程式的優先順序,數值越高,優先順序越低,越容易被殺死。
  • oom_adj threshold:在 Framework 層使用,代表 oom_adj 的記憶體閾值。Android Kernel 會定時檢測當前剩餘記憶體是否低於這個閥值,若低於則殺死 oom_adj ≥ 該閾值對應的 oom_adj 中,數值最大的程式,直到剩餘記憶體恢復至高於該閥值的狀態。
  • oom_score_adj: 在 Kernel 層使用,由 oom_adj 換算而來,是殺死程式時實際使用的引數。

oom_adj

取值範圍定義:

-> ProcessList(AOSP, master 分支)

    // These are the various interesting memory levels that we will give to
    // the OOM killer.  Note that the OOM killer only supports 6 slots, so we
    // can't give it a different value for every possible kind of process.
    private final int[] mOomAdj = new int[] {
            FOREGROUND_APP_ADJ, VISIBLE_APP_ADJ, PERCEPTIBLE_APP_ADJ,
            BACKUP_APP_ADJ, CACHED_APP_MIN_ADJ, CACHED_APP_MAX_ADJ
    };
複製程式碼

以上僅是 LMK 殺死程式時使用的 adj,實際上該類中定義了更多的 adj:

常量定義 常量取值 含義
NATIVE_ADJ -1000 native程式(不被系統管理)
SYSTEM_ADJ -900 系統程式
PERSISTENT_PROC_ADJ -800 系統persistent程式,比如telephony
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 後臺的重量級程式,system/rootdir/init.rc檔案中設定
SERVICE_ADJ 500 服務程式
HOME_APP_ADJ 600 Home程式
PREVIOUS_APP_ADJ 700 上一個App的程式
SERVICE_B_ADJ 800 B List中的Service(較老的、使用可能性更小)
CACHED_APP_MIN_ADJ 900 不可見程式的adj最小值
CACHED_APP_MAX_ADJ 906 不可見程式的adj最大值
UNKNOWN_ADJ 1001 一般指將要會快取程式,無法獲取確定值

以上常量在 Android 6.0(API23)及之前版本的取值範圍為 [-17, 16]:ProcessList(AOSP,marshmallow-release 分支)

規律:取值越大,重要性越低,程式越容易被殺死。

當觸發 LowMemoryKiller 機制時,可根據日誌中程式的 adj 值,具體分析程式是在什麼狀態下被殺死的。

oom_adj threshold

取值範圍定義:

-> ProcessList(AOSP, master 分支)

    // The actual OOM killer memory levels we are using.
    private final int[] mOomMinFree = new int[mOomAdj.length];
複製程式碼

具體取值由下面兩個變數經過換算得到:

    // These are the low-end OOM level limits.  This is appropriate for an
    // HVGA or smaller phone with less than 512MB.  Values are in KB.
    private final int[] mOomMinFreeLow = new int[] {
            12288, 18432, 24576,
            36864, 43008, 49152
    };
    // These are the high-end OOM level limits.  This is appropriate for a
    // 1280x800 or larger screen with around 1GB RAM.  Values are in KB.
    private final int[] mOomMinFreeHigh = new int[] {
            73728, 92160, 110592,
            129024, 147456, 184320
    };
複製程式碼

陣列初始化或更新的方法:

    private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
        float scaleMem = ((float)(mTotalMemMb-350))/(700-350);
        int minSize = 480*800;  //  384000
        int maxSize = 1280*800; // 1024000  230400 870400  .264
        float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
        float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
        ...省略
        final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
        for (int i=0; i<mOomAdj.length; i++) {
            int low = mOomMinFreeLow[i];
            int high = mOomMinFreeHigh[i];
            if (is64bit) {
                // Increase the high min-free levels for cached processes for 64-bit
                if (i == 4) high = (high*3)/2;
                else if (i == 5) high = (high*7)/4;
            }
            mOomMinFree[i] = (int)(low + ((high-low)*scale));
        }
        ...省略
        if (write) {
            ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
            buf.putInt(LMK_TARGET);
            for (int i=0; i<mOomAdj.length; i++) {
                // 除以了 PAGE_SIZE,所以 minfree 中的單位為頁,及 4KB
                buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);
                buf.putInt(mOomAdj[i]);
            }
            // 向 lmkd 程式傳送 LMK_TARGET 命令,
            // 將 oom_adj 閾值寫入 "/sys/module/lowmemorykiller/parameters/minfree" 
            // 將 oom_adj 寫入 "/sys/module/lowmemorykiller/parameters/adj"
            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
    }
複製程式碼

在 ActivityManagerService 呼叫 updateConfiguration() 的過程中會呼叫該方法,根據裝置的解析度初始化或更新閾值的大小。

oom_score_adj

取值範圍定義在 Linux Kernel 中:

-> oom.h

#ifndef _UAPI__INCLUDE_LINUX_OOM_H
#define _UAPI__INCLUDE_LINUX_OOM_H

/*
 * /proc/<pid>/oom_score_adj set to OOM_SCORE_ADJ_MIN disables oom killing for
 * pid.
 */
#define OOM_SCORE_ADJ_MIN	(-1000)
#define OOM_SCORE_ADJ_MAX	1000

/*
 * /proc/<pid>/oom_adj set to -17 protects from the oom killer for legacy
 * purposes.
 */
#define OOM_DISABLE (-17)
/* inclusive */
#define OOM_ADJUST_MIN (-16)
#define OOM_ADJUST_MAX 15

#endif /* _UAPI__INCLUDE_LINUX_OOM_H */
複製程式碼

oom_adj 到 oom_score_adj 的換算方法定義在 Linux Driver 中:

-> lowmemorykiller.c

static int lowmem_oom_adj_to_oom_score_adj(int oom_adj)
{
	if (oom_adj == OOM_ADJUST_MAX)
		return OOM_SCORE_ADJ_MAX;
	else
		return (oom_adj * OOM_SCORE_ADJ_MAX) / -OOM_DISABLE;
}
複製程式碼

工作流程

簡述 LMK 的工作流程

啟動 lmkd

lmkd 是由 init程式通過解析 init.rc 檔案來啟動的守護程式。lmkd 會建立名為 lmkd 的 socket,節點位於 /dev/socket/lmkd,該 socket 用於跟上層 framework 互動:

Android LowMemoryKiller  簡介

service lmkd /system/bin/lmkd
    class core
    group root readproc
    critical
    socket lmkd seqpacket 0660 system system
    writepid /dev/cpuset/system-background/tasks
複製程式碼

lmkd 啟動後,便會進入迴圈等待狀態,接受來自 ProcessList 的三個命令:

命令 功能 方法
LMK_TARGET 初始化 oom_adj ProcessList::setOomAdj()
LMK_PROCPRIO 更新 oom_adj ProcessList::updateOomLevels()
LMK_PROCREMOVE 移除程式(暫時無用) ProcessList::remove()

初始化 oom_adj

在 ActivityManagerService 呼叫 updateConfiguration() 的過程中會呼叫 ProcessList::updateOomLevels() 方法,根據裝置的解析度調整閾值的大小,通過 LMK_TARGET 命令,通知 lmkd(low memory killer deamon)分別向 /sys/module/lowmemorykiller/parameters 目錄下的 minfree 和 adj 節點寫入相應資訊:

Android LowMemoryKiller  簡介

更新 oom_adj

在 ActivityManagerService 呼叫 applyOomAdjLocked() 的過程中會呼叫 ProcessList::setOomAdj() 方法,通過 LMK_PROCPRIO 命令,通知 lmkd 向 /proc/程式號/oom_score_adj 寫入 oomadj:

D:\Android\projects\wanandroid_java>adb shell ps | findstr wanandroid
u0_a71    6461  1285  1177696 84024 SyS_epoll_ b131e424 S com.yuloran.wanandroid_java

D:\Android\projects\wanandroid_java>adb shell ls /proc/6461/
...省略
oom_adj
oom_score
oom_score_adj
...
複製程式碼

ActivityManagerService 會根據當前應用程式託管元件(即四大元件)生命週期的變化,及時的呼叫 applyOomAdjLocked(),更新程式狀態及該狀態對應的 oom_adj。

程式狀態表:

-> ActivityManager.java(AOSP,branch:master)

常量定義 常量取值 含義
PROCESS_STATE_UNKNOWN -1 非真實的程式狀態
PROCESS_STATE_PERSISTENT 0 persistent 系統程式
PROCESS_STATE_PERSISTENT_UI 1 persistent 系統程式,並正在執行UI操作
PROCESS_STATE_TOP 2 擁有當前使用者可見的 top Activity
PROCESS_STATE_FOREGROUND_SERVICE 3 託管一個前臺 Service 的程式
PROCESS_STATE_BOUND_FOREGROUND_SERVICE 4 託管一個由系統繫結的前臺 Service 的程式
PROCESS_STATE_IMPORTANT_FOREGROUND 5 對使用者很重要的程式,使用者可感知其存在
PROCESS_STATE_IMPORTANT_BACKGROUND 6 對使用者很重要的程式,使用者不可感知其存在
PROCESS_STATE_TRANSIENT_BACKGROUND 7 Process is in the background transient so we will try to keep running.
PROCESS_STATE_BACKUP 8 後臺程式,正在執行backup/restore操作
PROCESS_STATE_SERVICE 9 後臺程式,且正在執行service
PROCESS_STATE_RECEIVER 10 後臺程式,且正在執行receiver
PROCESS_STATE_TOP_SLEEPING 11 與 PROCESS_STATE_TOP 一樣,但此時裝置正處於休眠狀態
PROCESS_STATE_HEAVY_WEIGHT 12 後臺程式,但無法執行restore,因此儘量避免kill該程式
PROCESS_STATE_HOME 13 後臺程式,且擁有 home Activity
PROCESS_STATE_LAST_ACTIVITY 14 後臺程式,且擁有上一次顯示的 Activity
PROCESS_STATE_CACHED_ACTIVITY 15 程式處於 cached 狀態,且內含 Activity
PROCESS_STATE_CACHED_ACTIVITY_CLIENT 16 程式處於 cached 狀態,且為另一個 cached 程式(內含 Activity)的 client 程式
PROCESS_STATE_CACHED_RECENT 17 程式處於 cached 狀態,且內含與當前最近任務相對應的 Activity
PROCESS_STATE_CACHED_EMPTY 18 程式處於 cached 狀態,且為空程式
PROCESS_STATE_NONEXISTENT 19 不存在的程式

同一個程式,在不同狀態下,其 oom_adj 是不一樣的。

kill 並移除程式

在 ActivityManagerService 呼叫 updateOomAdjLocked() 時,會判斷程式是否需要被殺死,若是,則呼叫 ProceeRecord::kill() 方法殺死該程式:

Android LowMemoryKiller  簡介

: 目前 LMK_PROCREMOVE 命令暫時無用,即未執行有意義的程式碼。

結語

Android 記憶體系列至此,全部完結?。如有錯誤,還望不吝賜教?。

參考資料

相關文章