45、低内存管理:LMK、OOM Killer、zRAM、swap、内存压缩

说到Android的低内存管理,我脑子里第一个蹦出来的画面,就是几年前在项目里调试一个诡异的卡顿问题。用户反馈说手机用着用着突然就卡死了,点哪儿都没反应,过个十几秒又恢复正常。当时我抓了各种日志,最后发现是低内存场景下,系统在疯狂回收和压缩内存,CPU被占满了,UI线程根本抢不到时间片。

说白了,Android设备的内存是有限的,尤其是那些只有4GB、6GB的机型。当内存不够用时,系统必须有一套机制来「请走」不重要的进程,或者把不常用的数据压缩一下,腾出空间给前台应用。这套机制,就是我们今天要聊的LMK、OOM Killer、zRAM、swap和内存压缩。

1. LMK(Low Memory Killer)—— 内存不够时的「清道夫」

LMK是Android内核里一个很经典的机制。它的工作逻辑其实很简单:系统给每个进程分配一个「oom_score_adj」值,这个值越高,说明进程越不重要,越容易被杀掉。当系统可用内存低于某个阈值时,LMK就会开始干活,从oom_score_adj最高的进程开始杀。

我记得有一次,客户反馈说他们的App在后台待了一会儿就被杀了。我一看日志,发现App的oom_score_adj被系统调到了900多,而前台App只有0。这其实不是Bug,是LMK的正常行为——后台进程优先级低,内存紧张时先杀它。

核心要点:LMK的触发阈值和杀进程的优先级,由内核参数决定。常见的参数有:

  • /sys/module/lowmemorykiller/parameters/minfree —— 内存阈值列表
  • /sys/module/lowmemorykiller/parameters/adj —— 对应的oom_score_adj列表

举个例子,假设minfree设置为[18432, 23040, 27648, 32256, 55296, 80640](单位是页,一页4KB),adj设置为[0, 100, 200, 300, 900, 906]。当可用内存低于80640页(约315MB)时,系统就会杀掉oom_score_adj >= 906的进程。嗯,这里要注意,不同厂商的配置差异很大,有些厂商会调得很激进。

2. OOM Killer —— 最后的「核武器」

如果LMK没来得及处理,或者内存压力突然暴增,系统就会触发OOM Killer。OOM Killer是内核层面的机制,它的逻辑比LMK更粗暴——直接遍历所有进程,算出一个「badness」分数,然后杀掉分数最高的那个。

你可能会问:「那OOM Killer和LMK有什么区别?」

说白了,LMK是主动的、预防性的,它在内存还没完全耗尽时就动手。而OOM Killer是被动的、兜底的,只有当内存真的分配不出来时才会触发。我在项目中遇到过一种情况:某个App有内存泄漏,持续申请内存,LMK杀了好几个后台进程都没用,最后OOM Killer出手,直接把那个泄漏的App给杀了。

避坑指南:我曾经在调试一个系统稳定性问题时,发现OOM Killer杀错了进程。原因是那个进程的「badness」分数被某个内核模块错误地抬高了。后来我查了内核代码,发现OOM Killer的评分逻辑里,root进程和系统关键进程有保护机制,但第三方App没有。所以如果你的App被OOM Killer误杀,先检查一下它是不是申请了太多匿名内存。

3. zRAM —— 内存压缩的「瑞士军刀」

zRAM是Android里用得最广的内存压缩技术。它的原理很简单:在内存里划出一块区域作为压缩缓存,当系统需要回收匿名页时,不直接写到磁盘,而是压缩后存到zRAM里。这样既节省了内存,又避免了磁盘I/O的延迟。

我个人习惯在调试性能问题时,先看一眼zRAM的使用情况。命令很简单:

# 查看zRAM状态
cat /sys/block/zram0/mm_stat

# 输出示例(单位:字节)
# 原始数据大小  压缩后大小  元数据大小  最大可用  已用  零页数
# 104857600     52428800    2097152     209715200 52428800 1024

你看,原始数据100MB,压缩后只有50MB,节省了一半空间。但代价是什么?压缩和解压缩需要CPU时间。如果zRAM使用率太高,CPU会被压缩任务占满,导致系统卡顿。我遇到过一台设备,zRAM压缩率只有1.2倍,但CPU占用率飙升到80%,最后我建议厂商换了一个压缩算法(从lzo换成lz4),情况才好转。

小技巧:zRAM的压缩算法可以在编译内核时选择。常见的算法有lzo、lz4、zstd。lz4的压缩速度最快,但压缩率略低;zstd的压缩率最高,但CPU开销大。我个人推荐用lz4,因为Android设备对响应速度更敏感。

4. Swap —— 内存交换的「老古董」

Swap在Linux世界里是个老概念了,就是把内存里的数据换到磁盘上。但在Android里,Swap用得很少,因为手机用的是闪存,写入寿命有限,而且闪存的随机写入速度远不如内存。

不过,有些厂商会在低端机上启用Swap,配合zRAM一起用。逻辑是这样的:zRAM先压缩,如果压缩后还是放不下,就把数据换到Swap分区里。但说实话,我见过的大部分项目都只用了zRAM,没用Swap。为什么呢?因为一旦触发Swap,系统的响应速度会急剧下降,用户体验很差。

你想想看,一个App在后台被换到闪存上,用户切回来时,系统要从闪存读回来,那速度...嗯,我经历过一次,等了快两秒才看到界面。

5. 内存压缩 —— 不止zRAM

除了zRAM,Android里还有别的内存压缩技术,比如KSM(Kernel Same-page Merging)和Zcache。KSM会扫描内存中的相同页面,合并成一份,节省空间。Zcache则是在块设备层做压缩缓存。

但说实话,这些技术在手机上用得不多。KSM的扫描开销太大,不适合移动设备。Zcache的收益也不明显。我个人觉得,对于Android系统来说,zRAM + LMK的组合已经能解决大部分低内存问题了。

知识体系总览

下面这张图,是我梳理的低内存管理核心逻辑。你可以看到,从内存压力产生到最终处理,每一步都有对应的机制。

Android 低内存管理核心流程 内存压力产生 可用内存 < 阈值? LMK 杀进程 zRAM 压缩匿名页 OOM Killer 兜底 注:Swap 在 Android 中较少使用,故未在流程图中体现

总结一下

低内存管理这块,说白了就是一套「分级响应」的机制。轻度压力时,zRAM压缩一下;压力再大,LMK开始杀后台进程;实在不行了,OOM Killer出来兜底。Swap和KSM这些技术,在手机上用得少,但了解一下也没坏处。

我个人觉得,作为调试工程师,最需要关注的是zRAM的压缩率和LMK的阈值配置。这两个参数直接决定了系统在低内存场景下的表现。如果你发现设备卡顿,先看看zRAM是不是满了,再看看LMK有没有频繁杀进程。嗯,基本上就能定位到问题了。

一句话总结:LMK是主动清理,OOM Killer是最后防线,zRAM是压缩缓存,Swap是磁盘交换。四者配合,构成了Android的低内存管理体系。


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