内存管理(lmkd与lowmemorykiller)
内存管理这个话题,说实在的,是Android系统里最让人头疼的一块。我早期做Framework开发时,经常半夜被叫起来看OOM问题。那时候系统动不动就杀应用,用户骂声一片。后来lmkd(Low Memory Killer Daemon)出现,情况才好转不少。
今天我们就来聊聊lmkd的工作原理、内存阈值、进程优先级,以及内存泄漏检测。这些东西搞懂了,你就能理解系统是怎么在内存紧张时「断臂求生」的。
lmkd工作原理
lmkd是什么?说白了,它是一个守护进程,专门盯着系统内存使用情况。一旦发现内存快不够了,它就根据优先级杀掉一些进程,腾出空间。
我习惯把lmkd比作「内存管家」。它不像旧版lowmemorykiller那样简单粗暴——旧版是在内存分配时同步检查,而lmkd是异步的,更智能。
核心区别:
- 旧版lowmemorykiller:内核态,同步执行,内存分配时触发
- lmkd:用户态守护进程,异步监控,通过epoll监听内存事件
lmkd的工作流程大致是这样的:
- 注册内存压力通知(通过vmpressure或psi接口)
- 收到通知后,检查当前内存水位
- 根据预设阈值,决定是否需要杀进程
- 如果需要,遍历进程列表,找到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销毁时取消注册。
代码层面,我建议养成几个好习惯:
- 使用弱引用(WeakReference)持有Context或Activity
- 在onDestroy中清理所有注册的监听器和回调
- 避免非静态内部类持有外部类引用(比如Handler写成静态内部类)
- 使用Lifecycle组件自动管理生命周期
这里我画了一张图,帮你理清lmkd的整体工作流程:
最后说一句,内存泄漏检测不是一劳永逸的事。我建议在CI流程中加入LeakCananry的自动化检测,每次构建都跑一遍。这样能尽早发现问题,而不是等到线上用户反馈才去排查。
嗯,关于lmkd和内存管理,今天就聊这么多。这些知识在实际工作中非常实用,尤其是做系统优化或者应用性能调优的时候。多动手看看/proc下的文件,多分析几次heap dump,慢慢你就能摸清系统的脾气了。