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高于该阈值的进程。

来看一张图,理解这个流程:

LMK 内存水位线与进程优先级决策流程 可用内存 0 MB 水位线1 水位线2 水位线3 总内存 可用内存 < 水位线? 杀 oom_score_adj > 阈值的进程 水位线越低,杀掉的进程优先级越高(越不重要)

嗯,这里要注意:oom_score_adj的值越小,进程越重要。比如前台App的adj值通常是0,而后台Service可能是几百,空进程可能上千。

避坑指南:我曾经在调试一个第三方ROM时,发现系统总是莫名其妙杀掉前台App。查了半天,原来是厂商把前台App的adj值设成了200。所以,如果你遇到类似问题,先检查adj值对不对。

12.3 lmkd:用户空间的内存管理守护进程

lmkd是Android 9引入的。它替代了内核中的LMK驱动,负责监听内存压力事件并做出决策。

它的工作流程大致是这样的:

  1. 监听内核的vmpressure事件(内存压力通知)
  2. 读取/proc/meminfo获取当前内存状态
  3. 根据预设策略,计算需要释放多少内存
  4. 遍历进程列表,找到合适的“受害者”
  5. 发送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提供的一个工具,用来分析进程的内存布局。它比topdumpsys 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压缩率过高是预警信号

下次遇到手机杀后台或者卡顿,你知道该从哪里入手了吧?


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