2. Android内存管理基础
理解LMKD(Low Memory Killer Daemon)的调优逻辑,必须先建立Android内存管理的基础模型。Android基于Linux内核,其内存体系并非“一块连续的大内存”,而是由物理内存、虚拟地址空间、页表、映射关系和回收机制共同构成的动态系统。只有看清“谁在占用、占用的是哪类页、能否被回收”,才能理解为什么LMKD会在特定时刻杀进程,以及调优参数为何要那样设置。
一、物理内存与虚拟内存:两层视角
物理内存是设备上真实存在的RAM芯片容量,例如8GB、12GB。它被内核划分为一个个固定大小的页框(page frame),通常每页4KB。所有进程、内核代码、驱动缓冲、GPU共享内存最终都要落在这些页框上。
虚拟内存则是每个进程看到的独立地址空间。32位进程通常有3GB用户空间,64位进程地址空间极大。虚拟地址并不直接等于物理地址,中间要经过页表(page table)翻译。这样做带来三个关键好处:进程隔离、按需分配、以及可以用磁盘或压缩内存作为“后备存储”。
在Android中,虚拟内存的“后备”通常不是传统swap分区,而是zRAM——一块被压缩后存放在RAM中的交换区。因此Android的内存压力往往表现为:物理内存被匿名页和文件页共同占据,zRAM压缩率下降,可用内存逼近阈值,LMKD开始介入。
二、分页机制与缺页异常
分页机制把虚拟地址和物理地址都切成固定大小的页,通过多级页表建立映射。当CPU访问一个虚拟地址时,MMU查询页表:若映射存在且页在物理内存中,直接访问;若不存在,触发缺页异常(page fault),由内核决定是分配新页、从文件读取,还是从zRAM解压。
缺页异常分为两类:
- 次缺页(minor fault):页已在内存中,只是页表项未建立,或需要从zRAM解压。代价较小。
- 主缺页(major fault):页不在内存中,需要从存储或zRAM读取数据。代价较大,会阻塞进程。
Android启动应用时,主缺页过多会直接导致启动变慢。调优时关注/proc/vmstat中的pgmajfault,可以判断某进程是否频繁触发主缺页。
# 查看系统缺页统计
cat /proc/vmstat | grep -E "pgfault|pgmajfault"
# 查看某进程内存映射与缺页情况
cat /proc/<pid>/stat | awk '{print "minflt="$10, "majflt="$12}'
三、内存映射:文件页与匿名页的分界
内存映射(mmap)是Linux管理内存的核心手段。进程通过mmap把文件、设备或匿名区域映射到自己的虚拟地址空间。按“背后有没有文件”来分,内存页分为两大类:
- 文件页(file-backed page):背后对应磁盘上的文件,例如APK中的代码段、so库、资源文件、数据库页。干净的文件页可以直接丢弃,需要时再从文件读回,因此回收代价低。
- 匿名页(anonymous page):没有文件后备,例如Java堆、native堆、栈、malloc分配的内存。匿名页不能直接丢弃,必须先写入zRAM或swap,回收代价高。
Android的/proc/meminfo中,Cached主要反映文件页,AnonPages反映匿名页,SwapCached反映已换出到zRAM的匿名页。LMKD的杀进程策略,本质上是在匿名页无法快速回收时,通过终止进程来释放其占用的匿名页和文件页。
| 页类型 | 典型来源 | 回收方式 | 回收代价 | 相关指标 |
|---|---|---|---|---|
| 文件页(干净) | APK、so、资源、只读文件 | 直接丢弃,需要时重读 | 低 | Cached |
| 文件页(脏) | 数据库写入、日志 | 先回写,再丢弃 | 中 | Dirty、Writeback |
| 匿名页 | Java堆、native堆、栈 | 压缩到zRAM或换出 | 高 | AnonPages、SwapCached |
| 共享内存 | ashmem、GPU、Surface | 引用计数归零后释放 | 中 | Shmem、Mapped |
四、内存回收基本概念:kswapd、直接回收与LMKD
当物理内存不足时,内核按层次触发回收:
- 快速路径:分配内存时若空闲页充足,直接分配,不回收。
- kswapd后台回收:空闲页低于低水位线(low watermark)时,内核线程kswapd被唤醒,异步扫描LRU链表,回收文件页和匿名页。
- 直接回收(direct reclaim):若分配时仍无法满足,进程自己进入同步回收,阻塞等待,可能导致卡顿。
- OOM Killer / LMKD:当回收仍无法释放足够内存,内核OOM Killer或用户空间LMKD选择终止进程。
Android在传统OOM Killer之外引入LMKD,是因为移动设备需要更早、更可控地杀进程:在系统尚未完全卡死前,根据进程优先级(oom_score_adj)和内存压力,主动回收。LMKD监听/proc/meminfo、PSI(Pressure Stall Information)等信号,当可用内存或压力超过阈值时,向内核发送kill请求。
# 查看内存水位与回收情况
cat /proc/zoneinfo | grep -E "min|low|high|free"
# 查看PSI内存压力
cat /proc/pressure/memory
# 查看LMKD日志(需root或userdebug)
logcat -s lmkd
AnonPages与Cached的比例,比只看“剩余内存”更有意义。
五、案例与常见误区
案例:某应用启动后频繁被LMKD杀死。查看/proc/meminfo发现AnonPages持续升高,Cached下降,SwapCached增长缓慢。说明该应用分配了大量匿名页,且zRAM压缩效率不高,系统回收压力大。优化方向是减少Java堆峰值、复用Bitmap、避免大数组常驻,而不是简单调大LMKD阈值。
误区一:“剩余内存越多越好。”实际上,Linux会尽量利用空闲内存做文件缓存,MemFree低并不一定代表危险,关键看MemAvailable和匿名页回收压力。
误区二:“杀进程就能解决内存问题。”如果被杀进程持有大量共享内存或正在写文件,杀进程可能带来数据丢失和重新加载开销,LMKD需要结合oom_score_adj谨慎选择目标。
误区三:“虚拟内存越大越好。”64位进程地址空间很大,但物理页仍然有限,虚拟内存大不代表能同时驻留。
MemAvailable、AnonPages、Cached、SwapCached。只看一项容易误判。LMKD调优的本质,是在匿名页回收代价、文件页丢弃代价和进程重启代价之间做平衡。
掌握物理内存、虚拟内存、分页、映射、匿名页/文件页和回收路径这六个基础概念后,再去看LMKD的ro.lmk.*参数、PSI阈值和oom_score_adj分级,就能理解每个参数背后的内存语义,而不是机械地调大或调小。