资讯动态

Android查看应用oom_adj值:洞悉后台进程优先级与回收机制

发布时间:2026/10/18 4:19:53 来源:尧图企业网站定制
先说说我为什么开始盯着这个值。有一次我手里的应用在线上反馈里出现一种诡异现象用户按 Home 键去回了个消息再切回来整个页面全部重建之前的操作进度全丢了。当时第一反应是 Activity 被系统回收了但回收的时机和触发条件一直说不清。后来我养成了一个习惯——用 adb 连上设备盯着/proc/pid/oom_adj和/proc/pid/oom_score_adj看变化。这下问题一下子就清楚了就在按下 Home 键的下一秒那个进程的 adj 值从 0 直接跳到了 700 多待在后台没一会儿就被系统按“优先级最低”处理掉了。如果你也想搞清楚自己的应用到底在系统眼里“有多重要”或者想弄明白后台进程为什么总是“神秘消失”学会查看和记录应用的 oom_adj 值是绕不开的第一步。这篇内容就是围绕这件事写的怎么理解这个值、怎么查看它、怎么通过一段简单脚本记录它的变化以及我在真机上整理出来的一份完整观察记录。适合做 Android 应用开发、系统稳定性分析、以及想优化进程存活率的开发者参考。1. 先搞懂 oom_adj 在系统里到底管什么1.1 进程被杀的“数学题”OOM 机制是怎么运作的Android 系统虽然底层是 Linux 内核但它并没有完全照搬桌面 Linux 那套进程管理方式。桌面环境下进程一般是你手动开、手动关内存不够了才轮到内核出手。但手机不一样应用打开后台就常驻用户也不习惯手动清理任务系统必须在内存不知不觉吃紧的时候自己决定“干掉谁”。Linux 内核里有一套机制叫 OOM KillerOut Of Memory Killer。它的核心逻辑很简单当内存分配实在无法满足时内核根据每个进程的oom_score打分分数最高的进程优先被杀死。oom_score不是一个随意的分数它至少由两部分构成一是这个进程“占了多少内存”二是它“有多重要”。重要性就体现在oom_score_adj这个值上进程越重要这个值越小甚至为负数进程越不重要、越可以被牺牲这个值就越大。oom_score_adj会被叠加进oom_score的最终计算里当内存告急时系统按分数从高到低找“祭品”。Android 在这套机制上做了一层更细的封装。它把系统中各种角色统统分了个级前台应用、可见应用、感知进程比如播放音乐的后台应用、服务进程、缓存进程等等每个层级对应一个oom_score_adj区间。这个分级结果就是你在/proc文件系统里看到的oom_adj/oom_score_adj。所以查看应用的 oom_adj 值本质上就是在查看“系统认为这个应用当前处于什么地位以及它距离被杀死还有多远”。这里必须强调一点Android 的进程优先级管理并不只是“内存不够才去杀”而是从进程开始运行的每一刻都在动态调整。你的应用从启动、切后台、再回到前台每一次状态变化ActivityManagerService都会重新计算并设置这个值。这个动作在系统源码里通常叫setOomAdjLocked。所以你在脚本里看到的 adj 数字曲线其实就是系统对你应用“生命价值”的实时评估曲线。1.2 新旧两个 adj 接口看哪个才对任何玩过 Android 4.x、5.x 的老开发第一次接触这个概念时大多是从/proc/pid/oom_adj这个路径开始的。当年它的范围是 -16 到 15数字越小越重要负数通常留给系统核心进程。后来内核版本更新引入了新的接口/proc/pid/oom_score_adj范围扩大为 -1000 到 1000而且内核在计算 oom_score 时真正采用的就是这个新值。问题来了你在设备上同时cat /proc/pid/oom_adj和cat /proc/pid/oom_score_adj会发现两个数字看起来毫无规律好像对不上号。这是因为内核为了向后兼容保留了对旧接口的读取支持但内部会做一个线性映射换算。你可以简单理解为旧的oom_adj范围更窄新的oom_score_adj范围更宽内核在两者之间做了一次等比缩放。所以当你看到oom_adj是 0、oom_score_adj也是 0这只是巧合更多时候比如旧值是 7新值可能就是几百。真正的权威指标永远以oom_score_adj为准。我自己的习惯是如果只是想快速确认进程的“重要层级”直接看/proc/pid/oom_score_adj不要看oom_adj。因为 Android 的框架层代码里从早期版本到现在的ProcessRecord进程记录对象保存的也都是oom_score_adj的数值。旧oom_adj只适合查阅历史资料或者做兼容性验证。下文所有观察记录我也统一基于oom_score_adj来解读。2. 查看应用 oom_adj 值的三种姿势与权限注意事项2.1 单进程查看cat 一下最快如果你只是想知道某个具体应用当前排在哪一级用 adb 执行两条命令就够了adb shell ps -A | grep com.example.demoapp adb shell cat /proc/pid/oom_score_adj第一条命令拿到目标应用的 PID第二条命令直接读值。举个例子冷启动一个应用后立刻去查大概率看到0这是前台应用的标志。如果按 Home 键让它退到后台等两三秒再查数值会立刻升高可能是七八百甚至九百多。这个“升高”的过程不是瞬间完成的系统会经历一个状态转移的窗口期有些时候你查得太快看到的还是 0会误以为它没有被降级。建议切后台后稍等再查。这里有个细节oom_score_adj文件本身对普通用户可读但部分系统进程或受限目录在 selinux 策略下可能访问受限。我遇到过某些机型上直接cat报Permission denied这通常不是文件权限问题而是 selinux 对 shell 域的限制。遇到这种情况不用慌换下面的dumpsys方式或者把 adb 切到 root 身份如果是开发机或带 root 的测试机。2.2 全局快照dumpsys activity processes 更能看清全貌单看一个进程的 adj 值就像只看一张牌全局局面完全不清楚。更好的方式是用系统的调试服务直接拉一份所有进程的优先级快照adb shell dumpsys activity processes输出内容非常长但核心信息很直观。你会看到类似下面这样的进程列表ACTIVITY MANAGER RUNNING PROCESSES (pid 1234) Process LRU list (sorted by oom_adj): Proc # 0: adj0 /A /trm 0 (fg-app) com.example.demoapp/... Proc # 2: adj900 /C /trm 0 (cch) com.example.background/...这里每一行前面的adj就是oom_score_adj的当前值。列表是按 adj 排序的越靠前越安全越靠后的进程越容易被系统在内存紧张时回收。这个命令最大的好处是你不需要提前知道 PID也不需要逐个遍历一眼就能看到全部应用的相对排序。对于做内存回收分析、进程保活策略设计来说这是排查问题的第一工具。顺带说一句dumpsys activity o也会输出类似内容但比较老练的开发会更喜欢看dumpsys activity processes因为里面不仅包含 adj还包含每个进程的ProcState状态、已连接 Activity 数量、进程是否处于 cached 状态等关联字段。这些信息配合 adj 一起解读才能还原完整场景。举个例子一个进程 adj 是 900但同时显示它有一个前台服务那说明它正处于一种“看起来可以回收但又有活跃任务”的中间态。2.3 批量脚本一次拿到所有进程的 adj 和进程名dumpsys适合人工看但如果想持续记录、分析变化趋势还是得写脚本批量遍历。遍历的思路很直接Linux 的每个进程都会在/proc下生成一个以 PID 为名的目录我们循环读这些目录里的oom_score_adj和comm进程名就行。下面这段 shell 代码可以直接通过 adb 执行adb shell for p in /proc/[0-9]*; do pid${p#/proc/}; adj$(cat /proc/$pid/oom_score_adj 2/dev/null); name$(cat /proc/$pid/comm 2/dev/null); echo \$pid|$adj|$name\; done这段命令会输出三列PID、adj 值、进程名。输出后你可以用sort按 adj 值排个序就能立刻看到当前系统里“最危险”的进程是谁。我通常在内存优化类问题排查时会先把输出重定向到本地文件adb shell for p in /proc/[0-9]*; do pid${p#/proc/}; adj$(cat /proc/$pid/oom_score_adj 2/dev/null); name$(cat /proc/$pid/comm 2/dev/null); echo \$pid|$adj|$name\; done adj_snapshot.txt这个批量脚本虽然简单但非常实用。尤其是当内存告急、系统开始批量杀进程时拿两份不同时间点的快照做 diff哪些进程被杀了、哪些进程 adj 被提高了一目了然。再补充一个更省事的组合技如果想监控某个包名下的所有进程冷门情况比如多进程应用可以用pidof拿到所有 PID 再逐个读取。后面第三部分的观察脚本里我会给出一个带时间戳的完整版本那才真正适合长期观察记录场景。3. 实操写一个可复用的 oom_adj 观察脚本3.1 脚本设计与每段命令的意图我实际用来做“查看应用 oom_adj 值观察记录”的脚本不是一次性命令而是一个带循环、带时间戳、带进程状态采集的小工具。核心需求有三个持续输出目标应用在不同操作下的 adj 变化同时记录内存占用和进程状态方便关联分析当进程被杀时日志里能明确看到“进程消失”的时点。下面这段脚本可以直接在 adb shell 里运行也可以在设备上的终端模拟器里跑。我注释了每一段的用途#!/system/bin/sh # 观察脚本持续打印目标应用的 oom_score_adj 变化 PKGcom.example.demoapp INTERVAL2 echo 开始观察 $PKG采样间隔 ${INTERVAL}s while true; do TS$(date %Y-%m-%d %H:%M:%S) PIDS$(pidof $PKG) if [ -z $PIDS ]; then echo $TS | 进程未运行可能已被回收 else for PID in $PIDS; do SADJ$(cat /proc/$PID/oom_score_adj 2/dev/null) OADJ$(cat /proc/$PID/oom_adj 2/dev/null) RSS$(grep ^VmRSS: /proc/$PID/status 2/dev/null | awk {print $2}) STAT$(awk {print $3} /proc/$PID/stat 2/dev/null) NAME$(cat /proc/$PID/comm 2/dev/null) echo $TS | pid$PID | oom_score_adj$SADJ | oom_adj$OADJ | rss${RSS}kB | state$STAT | $NAME done fi sleep $INTERVAL done简单解释一下几个采集项oom_score_adj权威 adj 值主要观察指标。oom_adj旧接口值顺便记录用于对比新旧映射观察时不会频繁看它但偶尔会用在验证上。VmRSS进程当前实际占用的物理内存。为什么要同时记录因为 OOM 分数不仅看 adj 还看内存占用两个进程 adj 相同比如都是 900系统优先杀内存占用更大的那个。只看 adj 不看内存会漏掉一半信息。stat的第三字段进程当前内核状态。R表示运行中S表示可中断睡眠D表示不可中断睡眠Z表示僵尸进程。在观察早期的“冷启动阶段”你会看到进程状态频繁变动这也能辅助判断系统正在做什么。脚本里有个小细节pidof可能返回多个 PID所以用for循环逐个处理。现在的应用大多是多进程架构主进程、:push进程、:remote进程各自有独立的 adj 值系统对它们的管理策略也彼此独立。很多开发者只盯着主进程看结果漏掉了后台进程被单独回收的情况。3.2 操作流程让观察脚本与你的操作联动脚本本身只是一个记录器真正的关键在于“记录与操作的联动”。我建议把观察过程拆成几个明确的阶段每个阶段做什么操作、预期看到什么变化提前列清楚。下面是我经常用的操作流程第一步先把目标应用杀掉确保从冷启动开始观察。执行adb shell am force-stop com.example.demoapp然后启动脚本等一两行输出确认进程不在。第二步冷启动观察。手动点击应用图标让应用启动脚本会捕捉到进程从无到有的过程。这里有一个非常有意思的细节进程刚被zygotefork 出来时adj 值不是 0而是某个未设置的高值不同系统版本表现不同常见的是 1000 左右直到应用完成 attach、ActivityManagerService完成该进程的优先级登记adj 才会变成 0。这个过程只有几百毫秒采样间隔 2 秒可能刚好错过为了看到它可以把间隔临时改成 0.5 秒。第三步前台停留观察。保持应用在前台等 5~10 秒看 adj 是否稳定在 0。应用进入 idle 状态后有些系统会略微上调一下 adj但一般还在前台进程区间。第四步切后台观察。按 Home 键回到桌面继续盯着输出。正常情况下 adj 会在 1~3 秒内跳到 700 以上如果应用曾经在其他时间被标记为“最近任务里的前一个应用”这个值会先变到中间档再逐步上调到缓存区间。第五步重新唤起观察。从最近任务列表里再次点开应用看 adj 是否快速回落到 0。之后依次多切换几个应用观察“被切走的那个”是如何一步步在后台边缘化的。第六步内存压力观察。这是整个观察里最有价值的部分我会放在第四部分详细展开。你可以先在后台开满十几个应用再跑一个大内存测试看系统如何按 adj 从大到小回收进程。脚本里那些“进程未运行”的行就是系统出手杀人的证据。4. 一次完整的 oom_adj 观察记录与解读4.1 场景记录前台、可见、服务、缓存的状态变化以下是我在一台 8GB 内存的测试机上做的真实观察记录。目标应用包名统一写成com.example.demoapp进程仅主进程无多进程。我按时间线整理了关键节点系统版本为 Android 12注意不同 Android 版本的具体 adj 数值会有差异这里关注的是变化规律时间节点操作PIDoom_score_adj进程状态解读10:00:00force-stop 后启动脚本无无无应用未运行10:00:05点击桌面图标32101000 左右S刚 fork 出来优先级尚未设置10:00:06应用首帧显示32100S进入前台为最高优先级10:00:15在前台停留32100S稳定前台进程10:00:20按 Home 键回桌面3210700 左右S变成 previous app优先级大幅下降10:00:30从最近任务重新点开32100S重新回到前台adj 立即召回10:00:40切到另一个应用 B3210900~999S被挤到缓存后台区靠近回收线10:00:55打开相机等重内存操作3210进程消失无被系统回收之后点击图标重新冷启动这张表里最值得玩味的是 10:00:20 到 10:00:40 的变化。按 Home 键的瞬间应用并不会立刻跌到缓存区间而是先进入一个“前一个应用”的过渡状态adj 大概在几百这个量级。如果用户短时间内又切回来系统会因为它停留的层级还不够低快速恢复它的前台地位。一旦你继续切换到第三个应用第一个应用就会被继续挤到更靠后的位置一直滑到 900 以上。Android 对“最近使用过的应用”是有记忆的这个记忆反映在 adj 上就是那条从 0 → 700 → 900 的缓降曲线。另一个值得记录的场景是“可感知进程”。我把一个音乐类应用放在后台播放它的进程没有可见 UI但因为是前台服务播放音乐adj 通常被固定在 200 左右不同版本略微不同而不是像普通后台应用那样滑到 900。这个设计意图很明显用户虽然看不到界面但能听得到声音直接杀掉会导致体验断崖。记录这个场景可以帮助你理解为什么你的应用明明退到后台了系统还给它一个相对不错的优先级——很可能就是因为你调用过startForeground。4.2 低内存场景从记录中看懂 lmkd 的回收顺序前边说的都是前台后台切换最精彩的部分永远是内存吃紧的时候。观察当天我做了这样一组操作先连续打开 15 个普通应用然后打开一个内存占用奇高的应用很快系统就开始逐批清理后台进程。清理大体按 adj 从高到低进行先消失的永远是那些 adj 在 900~999 的缓存进程。如果你同时盯着 logcat还能看到系统杀进程时留下的记录不同版本关键字不太一样常见的有am_kill、lowmemorykiller、lmkd日志里通常包含被杀进程的 PID、进程名、adj 值和当时的内存信息。比如某次记录里出现了am_kill: 22130 com.example.background/.MainActivity (cached, 402th, 950)这行日志里的950就是当时进程的 oom_score_adj。这些日志和我们的脚本采样结果互相印证能把“系统先杀谁、后杀谁、为什么杀它”完整还原出来。我在观察过程中还发现了一个容易忽略的细节系统并不是等到内存彻底耗尽才动手而是基于内存压力水位线提前回收。你可以把系统内存想象成一个大水库当水位降到某个预警线lmkd就开始先放掉最不重要的水高 adj 进程水位继续下降就放掉更多中等重要的水如果还没缓解才会波及前台进程。所以你在脚本里看到的“进程消失”往往发生在内存还有余量的时候这恰恰说明 Android 的回收策略是偏预防性的而不是亡羊补牢型。实际操作时想快速复现低内存场景可以配合开发者选项里的“后台进程限制”。把它设成“不得超过 2 个进程”再疯狂切后台应用能很快看到系统频繁回收的实况。不过要记住观察结束后把这个选项改回标准限制否则后续测试结果都会被它干扰。5. 常见问题速查与避坑心得5.1 常见问题与排查路径看 oom_adj 这件事本身不难难的是过程中总会遇到一些奇奇怪怪的干扰导致记录结果不可信。我把常见问题整理成了表格按出现频率排序现象可能原因排查与解决cat /proc/pid/oom_score_adj报 Permission deniedselinux 限制 shell 读取部分进程换dumpsys activity processes或在开发机 root 下查看脚本输出里看不到目标进程 PID应用在采样间隙被杀或包名写错先用ps -A确认进程存在观察时调小采样间隔oom_adj 和 oom_score_adj 数值感觉对不上新旧接口采用不同映射区间一切以 oom_score_adj 为准旧值仅作兼容参考应用切后台很久adj 仍不升高应用持有前台服务或正在播放媒体检查是否有startForeground或可见通知脚本能跑但永远只有一行目标应用被强制停止或系统回收后没有自动重启强制停止后等首次进程出现时再开始观察模拟器上很难看到进程被杀模拟器配置内存较大回收阈值难以触发改用真机或调低后设备内存限制复现排查时有一个原则先确认时间线再下结论。很多人一看到 adj 高就认为是系统回收了进程但有时 PID 已经变了说明进程是被杀后重新冷启动的新进程旧进程早就没了。所以脚本里必须同时打印 PID 和时间戳光看包名很容易被新进程误导。5.2 我的实操笔记几个容易被忽略的细节最后分享几段踩过坑之后沉淀下来的个人笔记这几条在常规文档里很难找到。第一查看/proc/pid/oom_score_adj时进程状态是Z僵尸进程的情况要单独注意。僵尸进程本身已经死了它的 adj 值没有任何调度意义但因为它还没被父进程回收/proc目录里会残留一段时间的记录。等你看到 adj 很高而 PID 对应的进程状态是Z不用浪费时间分析它已经是一具“尸体”了。第二观察记录里要同时留下系统内存余量的信息。只看 adj 曲线你会发现一些“无法解释”的杀进程现象比如一个 adj 只有 200 的进程也被杀了。这时候查一下同一时刻的MemAvailable就会明白系统当时已经极度紧张连中等优先级的进程也不能幸免。所以我后来在脚本里加了内存采集建议你也加上adb shell cat /proc/meminfo | grep -E MemAvailable|MemFree第三不同 Android 版本的 adj 层级数值存在微调网上很多文章给出的对照表是基于旧版本系统的拿新版系统一比会发现对不上。我的做法是不背死数值而是记住层级逻辑——前台最小、缓存最大、系统核心进程为负值。观察时重点关注相对变化趋势而不是纠结某个具体数字。第四如果你只是想排查“为什么我的应用总是被杀”不要只在自己设备上观察最好多找几台不同内存档位的机器对比。小内存机器上系统回收激进大内存机器上回收很晚。同样的代码在两台机器上的存活表现可能天差地别这不是代码问题是系统策略不同。理解了这一点你才能在优化进程存活率时做出正确的取舍。关于查看应用 oom_adj 值这件事写到这基本把核心内容都覆盖了。我个人在实际操作中的体会是这套数值系统看懂了之后Android 后台进程那点“神秘感”会消失一大半。它没有什么黑魔法就是一套有明确规则的优先级调度算法。你只需要一个脚本、一台真机、一颗愿意记录的心就能把系统对每个应用的态度看得明明白白。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑