内存管理(lmkd与lowmemorykiller)

内存管理这个话题,说实在的,是Android系统里最让人头疼的一块。我早期做Framework开发时,经常半夜被叫起来看OOM问题。那时候系统动不动就杀应用,用户骂声一片。后来lmkd(Low Memory Killer Daemon)出现,情况才好转不少。

今天我们就来聊聊lmkd的工作原理、内存阈值、进程优先级,以及内存泄漏检测。这些东西搞懂了,你就能理解系统是怎么在内存紧张时「断臂求生」的。

lmkd工作原理

lmkd是什么?说白了,它是一个守护进程,专门盯着系统内存使用情况。一旦发现内存快不够了,它就根据优先级杀掉一些进程,腾出空间。

我习惯把lmkd比作「内存管家」。它不像旧版lowmemorykiller那样简单粗暴——旧版是在内存分配时同步检查,而lmkd是异步的,更智能。

核心区别:

  • 旧版lowmemorykiller:内核态,同步执行,内存分配时触发
  • lmkd:用户态守护进程,异步监控,通过epoll监听内存事件

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

  1. 注册内存压力通知(通过vmpressure或psi接口)
  2. 收到通知后,检查当前内存水位
  3. 根据预设阈值,决定是否需要杀进程
  4. 如果需要,遍历进程列表,找到oom_score_adj最大的进程杀掉

嗯,这里要注意:lmkd不是直接杀进程,而是通过内核的oom killer机制来执行。它只是「建议」内核杀谁。

内存阈值与回收策略

内存阈值,就是系统设定的几个水位线。我项目中遇到过一个问题:某款低端设备频繁卡顿,查了半天发现是lmkd阈值设得太保守,导致系统还没到真正危险就乱杀进程。

Android系统通常定义了几个内存等级:

内存等级 含义 典型行为
NORMAL 内存充足 不做任何操作
LOW 内存偏低 开始回收缓存页
MEDIUM 内存紧张 杀部分后台进程
CRITICAL 内存极度紧张 大规模杀进程

回收策略方面,lmkd会优先回收「最不值钱」的内存。什么叫不值钱?就是那些可以被快速重建的缓存数据。比如图片缓存、页面缓存等。

我曾经踩过一个坑:某个应用频繁被杀,用户反馈说「切出去回个微信,回来应用就重启了」。查下来发现,这个应用在后台持有大量Bitmap缓存,lmkd认为它占用了太多可回收内存,就优先把它杀了。解决方案是让应用在onTrimMemory回调里主动释放缓存。

避坑指南:如果你的应用经常被lmkd盯上,建议在onTrimMemory()中主动释放不重要的资源。别等系统来杀你,那体验太差了。

进程优先级与oom_score_adj

oom_score_adj,这是lmkd判断「先杀谁」的核心依据。值越大,越容易被杀。值越小,越安全。

Android系统给不同进程设定了不同的oom_score_adj范围:

进程类型 oom_score_adj范围 说明
系统核心进程 -1000 到 -900 几乎不会被杀
前台进程 0 到 100 用户正在交互
可见进程 100 到 200 用户可见但未交互
服务进程 200 到 300 后台运行服务
后台进程 300 到 900 不可见,随时可杀
空进程 900 到 1000 无任何组件运行

你想想看,为什么有时候杀进程会先杀后台应用?就是因为它们的oom_score_adj高。系统会从高到低排序,一刀刀砍下去,直到内存恢复到安全水位。

我个人习惯在调试时查看/proc/[pid]/oom_score_adj,直接看当前进程的优先级。比如:

adb shell cat /proc/1234/oom_score_adj
# 输出可能是 0(前台进程)或 900(空进程)

我记得有一次线上问题:某个系统服务被误杀了,导致系统UI重启。查下来发现这个服务的oom_score_adj被设成了200,而正常应该是-800。原因是某个模块在初始化时错误地修改了它的adj值。嗯,这种问题最难排查。

注意:不要随意修改进程的oom_score_adj。改低了可能导致系统无法回收内存,改高了可能导致重要进程被误杀。Android Framework已经有一套成熟的分配逻辑,除非你非常清楚自己在做什么。

内存泄漏检测

内存泄漏,说白了就是「该释放的内存没释放」。在Android上,最常见的是Activity泄漏、Bitmap泄漏、Handler泄漏。

我常用的检测手段有几种:

  • dumpsys meminfo:快速查看进程内存分布
  • LeakCanary:自动检测泄漏,集成方便
  • MAT(Memory Analyzer Tool):分析heap dump,定位泄漏根因
  • Android Studio Profiler:实时监控内存变化

举个例子,我曾经用LeakCanary发现一个诡异的泄漏:某个Fragment被销毁后,它的View仍然被一个静态的Drawable引用着。原因是这个Drawable里注册了回调,但没在Fragment销毁时取消注册。

代码层面,我建议养成几个好习惯:

  1. 使用弱引用(WeakReference)持有Context或Activity
  2. 在onDestroy中清理所有注册的监听器和回调
  3. 避免非静态内部类持有外部类引用(比如Handler写成静态内部类)
  4. 使用Lifecycle组件自动管理生命周期

这里我画了一张图,帮你理清lmkd的整体工作流程:

lmkd 内存管理核心流程 内存压力检测(vmpressure/psi) 判断内存水位(NORMAL/LOW/MEDIUM/CRITICAL) 遍历进程列表,按 oom_score_adj 从高到低排序 通过内核 OOM Killer 执行杀进程 关键参数 • oom_score_adj 范围 -1000 ~ 1000 • 内存水位阈值 由 /sys/fs/cgroup/ memory/ 配置 • 杀进程策略 从高adj到低adj • 回收目标 恢复到安全水位 • 监控接口 vmpressure / psi

最后说一句,内存泄漏检测不是一劳永逸的事。我建议在CI流程中加入LeakCananry的自动化检测,每次构建都跑一遍。这样能尽早发现问题,而不是等到线上用户反馈才去排查。

嗯,关于lmkd和内存管理,今天就聊这么多。这些知识在实际工作中非常实用,尤其是做系统优化或者应用性能调优的时候。多动手看看/proc下的文件,多分析几次heap dump,慢慢你就能摸清系统的脾气了。

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