12、内存管理机制:Low Memory Killer、lmkd、内存回收策略与Procmem分析
内存管理,说白了就是系统在有限的内存里,怎么让所有App都活得舒服。但Android不像PC,它没有Swap分区。内存一旦吃紧,系统就得“请”一些进程离开——这就是Low Memory Killer(LMK)的由来。
我最早接触LMK时,觉得它就是个“杀手”,简单粗暴。后来深入源码才发现,它背后是一整套精细的回收策略。今天我们就把它拆开看看。
12.1 从OOM到LMK:Android的进化之路
Linux内核原本有个OOM Killer,但它有个问题:它是在内存彻底耗尽时才触发。你想想看,手机卡死半天,然后突然黑屏重启——这体验太糟糕了。
Android的做法是:在内存还没耗尽之前,就提前杀掉低优先级的进程。这就是LMK的核心思想。
关键区别:
- OOM Killer:内存耗尽 → 杀进程(被动、滞后)
- LMK:内存水位线 → 杀进程(主动、提前)
我记得在Android 4.4时代,LMK还是内核中的一个独立驱动模块。到了Android 9,Google把它移到了用户空间,变成了lmkd守护进程。为什么这么做?因为用户空间的策略更灵活,可以随时更新,不用改内核。
12.2 LMK的工作机制:水位线与优先级
LMK把内存分成了几个水位线(watermark)。每个水位线对应一个oom_score_adj阈值。当可用内存低于某个水位线时,系统就会杀掉所有oom_score_adj高于该阈值的进程。
来看一张图,理解这个流程:
嗯,这里要注意:oom_score_adj的值越小,进程越重要。比如前台App的adj值通常是0,而后台Service可能是几百,空进程可能上千。
避坑指南:我曾经在调试一个第三方ROM时,发现系统总是莫名其妙杀掉前台App。查了半天,原来是厂商把前台App的adj值设成了200。所以,如果你遇到类似问题,先检查adj值对不对。
12.3 lmkd:用户空间的内存管理守护进程
lmkd是Android 9引入的。它替代了内核中的LMK驱动,负责监听内存压力事件并做出决策。
它的工作流程大致是这样的:
- 监听内核的vmpressure事件(内存压力通知)
- 读取/proc/meminfo获取当前内存状态
- 根据预设策略,计算需要释放多少内存
- 遍历进程列表,找到合适的“受害者”
- 发送SIGKILL信号
lmkd的配置文件在/sys/module/lowmemorykiller/parameters/下,但实际策略是由lmkd自己管理的。你可以通过dumpsys查看它的状态:
adb shell dumpsys activity lmkd
输出会告诉你当前的内存水位线、各进程的adj值、以及最近杀掉的进程列表。
12.4 内存回收策略:不只是杀进程
杀进程是最后的手段。在这之前,系统会尝试多种回收方式:
| 回收方式 | 触发条件 | 回收内容 | 影响 |
|---|---|---|---|
| kswapd | 内存低于水位线 | 页面换出、脏页回写 | 轻微卡顿 |
| 直接回收 | 内存分配失败 | 同步回收页面 | 明显卡顿 |
| 压缩(zram) | 内存压力中等 | 压缩匿名页 | CPU开销 |
| LMK/lmkd | 内存严重不足 | 杀掉整个进程 | 应用重启 |
我个人习惯先看zram的使用情况。如果zram压缩率很高(比如超过3:1),说明内存压力已经很大了,LMK随时可能触发。
12.5 Procmem分析:定位内存问题
Procmem是Android提供的一个工具,用来分析进程的内存布局。它比top和dumpsys meminfo更底层。
用法很简单:
adb shell procmem [pid]
输出会显示进程的各个内存段:
- VSS:虚拟内存大小(基本没用)
- RSS:常驻内存大小(包含共享库)
- PSS:按比例分摊的常驻内存(最准确)
- USS:独占内存(进程独享)
我一般在分析内存泄漏时,会重点关注USS。因为如果USS持续增长,说明进程在不停地分配内存但没有释放。而PSS增长可能是共享库的问题。
注意:Procmem在Android 10之后被移除了,改用showmap命令。但原理是一样的。如果你在调试老版本系统,Procmem依然好用。
12.6 实战:分析一次内存不足问题
有一次,用户反馈手机用一段时间后就变得特别卡,而且经常杀后台。我连上adb,先看内存状态:
adb shell cat /proc/meminfo
MemTotal: 3840000 kB
MemFree: 120000 kB
Cached: 800000 kB
SwapTotal: 800000 kB
SwapFree: 100000 kB
嗯,可用内存只有120MB,zram也快满了。再看lmkd日志:
adb logcat -b events | grep lmkd
lmkd: kill 12345 'com.example.app' oom_score_adj 900
lmkd: kill 12346 'com.example.service' oom_score_adj 800
发现被杀的都是adj值在800以上的进程。但奇怪的是,系统还在不停地杀。我查了一下,原来是某个App有内存泄漏,导致系统一直在回收内存。
用Procmem分析那个App:
adb shell procmem 12345
PSS USS Name
50000 45000 com.example.leakyapp
30000 28000 libc.so
20000 18000 libart.so
USS高达45MB,对于一个后台App来说明显偏高。最终定位到是Bitmap没有回收。修复后,系统内存压力明显下降。
12.7 总结一下
LMK和lmkd是Android内存管理的最后一道防线。理解它们的工作原理,能帮你快速定位内存相关的问题。
几个要点记住:
- LMK是提前杀,不是等死
- oom_score_adj越小越重要
- lmkd监听vmpressure事件
- Procmem看USS,别只看PSS
- zram压缩率过高是预警信号
下次遇到手机杀后台或者卡顿,你知道该从哪里入手了吧?