9、进程优先级与LRU算法:AMS如何管理进程优先级、adj值、lmk触发机制

进程优先级,说白了就是Android系统给每个App打的“生存分”。

分数高的,系统会优先保它;分数低的,内存不够时第一个被干掉。我刚开始接触这部分时,觉得不就是个优先级嘛,后来才发现,这里面的门道远比想象中复杂。

9.1 什么是adj值?

adj值,全称是oom_adj。它是Linux内核OOM Killer用来判断“先杀谁”的关键指标。

Android在原生Linux基础上,自己定义了一套adj值体系。范围从-17到15,数值越小,进程越重要。

adj值 进程类型 典型场景
-17 系统核心进程 init、zygote、system_server
0 前台进程 用户正在交互的Activity
1 可见进程 Activity部分可见,比如被对话框遮挡
2 服务进程 正在运行Service
3 后台进程 Activity已不可见
4 空进程 没有任何组件在运行
15 缓存进程 最容易被杀死的进程

嗯,这里要注意:adj值不是一成不变的。AMS会动态调整它。

比如一个App从前台切到后台,adj值会从0变成3。如果它又启动了Service,adj值可能变成2。我见过不少开发者以为进程优先级是固定的,结果在后台服务被频繁杀死时一脸懵。

9.2 AMS如何管理进程优先级?

AMS管理进程优先级,核心逻辑在ProcessListOomAdjuster这两个类里。

流程大致是这样的:

  1. AMS收到组件状态变化(比如Activity启动、Service绑定)
  2. 调用updateOomAdjLocked()方法
  3. 遍历所有进程,重新计算每个进程的adj值
  4. 通过ProcessList写入/proc/pid/oom_adj
  5. 内核根据adj值决定OOM优先级

我举个例子,你感受一下:

// 伪代码,展示AMS调整adj的核心逻辑
void updateOomAdjLocked() {
    for (ProcessRecord app : mProcessList) {
        int adj = computeOomAdj(app);
        // 写入内核
        writeOomAdjToProc(app.pid, adj);
    }
}

int computeOomAdj(ProcessRecord app) {
    if (app.hasForegroundActivities()) {
        return 0; // 前台进程
    }
    if (app.hasVisibleActivities()) {
        return 1; // 可见进程
    }
    if (app.hasActiveServices()) {
        return 2; // 服务进程
    }
    if (app.hasRecentActivities()) {
        return 3; // 后台进程
    }
    return 4; // 空进程
}

这段代码看着简单,但实际逻辑复杂得多。我记得在Android 8.0之前,AMS每次调整adj都要遍历所有进程,性能开销很大。后来Google优化成了增量更新,只调整状态变化的进程。

9.3 LRU算法在进程管理中的应用

LRU,全称Least Recently Used,最近最少使用算法。

AMS维护了一个LRU列表,用来记录进程的“活跃程度”。

具体来说:

  • 当进程被使用时,它会被移到LRU列表的头部
  • 当进程长时间未被使用,它会逐渐滑向列表尾部
  • 当需要回收内存时,从LRU列表尾部开始杀进程

这个LRU列表和adj值有什么关系呢?

我个人的理解是:adj值决定了进程的“优先级等级”,LRU决定了同一等级内进程的“淘汰顺序”。

举个例子:

你有两个后台进程,adj值都是3。但一个5分钟前被用过,另一个2小时前被用过。LRU列表会把2小时前的那个排在后面,内存不足时先杀它。

核心要点:adj值决定“能不能杀”,LRU决定“先杀谁”。两者配合,构成了Android进程管理的完整体系。

9.4 LMK触发机制

LMK,Low Memory Killer,低内存杀手。它是内核层的一个机制,但触发逻辑和AMS密切相关。

LMK的触发条件很简单:当系统可用内存低于某个阈值时,LMK开始干活。

但这里有个关键点:LMK不是随机杀进程的。它会参考AMS设置的adj值。

流程是这样的:

  1. AMS通过writeOomAdjToProc()把adj值写入/proc/pid/oom_adj
  2. LMK监控系统内存,当内存低于阈值时,遍历所有进程
  3. 找到adj值最大的进程(优先级最低的)
  4. 发送SIGKILL信号,杀掉该进程

我曾经遇到过一个问题:某个App在后台频繁被杀,用户反馈很强烈。排查后发现,这个App的Service一直在做高耗时的后台任务,导致adj值被AMS调整为2(服务进程)。但问题是,它占用的内存太大,LMK在内存紧张时还是会优先杀它。

避坑指南:如果你的App在后台容易被杀,不要只盯着adj值。检查一下内存占用是否合理。我曾经见过一个App,adj值只有1(可见进程),但因为内存泄漏,占用超过500MB,照样被LMK干掉。

9.5 整体流程可视化

说了这么多,我画张图帮你理清思路:

AMS进程优先级与LMK触发流程 AMS (ActivityManagerService) updateOomAdjLocked() OomAdjuster (计算adj值) writeOomAdjToProc() Linux内核 (proc/pid/oom_adj) 内存不足时触发 LMK (Low Memory Killer) 关键数据结构 • ProcessList • ProcessRecord • oom_adj (-17~15) • LRU列表 • 内存阈值 adj值决定优先级 LRU决定淘汰顺序 LMK执行最终裁决

这张图把整个流程串起来了。从AMS计算adj值,到写入内核,再到LMK触发,每一步都环环相扣。

9.6 实际项目中的经验

我在项目中遇到过一个问题,想分享给你:

有一次,一个视频类App在后台播放音乐时,总是被系统杀死。用户反馈说“听着听着就停了”。

排查过程是这样的:

  • 先看adj值:App启动了前台Service,adj值应该是2
  • 再看内存:App占用了800MB,因为缓存了多个视频流
  • 再看LRU:App虽然adj值低,但LRU列表中排在很后面

最终结论:LMK在内存紧张时,虽然adj值低,但内存占用太大,加上LRU位置靠后,还是被杀了。

注意:adj值不是万能的。如果你的App内存占用过大,即使adj值很低,LMK也可能在极端情况下杀掉它。我建议你在开发时,定期检查App的内存占用,避免无谓的缓存。

好了,关于进程优先级和LMK,今天就聊到这里。记住一句话:adj值是基础,LRU是辅助,LMK是最终执行者。三者配合,构成了Android进程管理的完整体系。


公众号:蓝海资料掘金营,微信deep3321