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开始介入。

关键结论:物理内存决定“能装多少”,虚拟内存决定“每个进程能看到多少”,页表决定“两者如何对应”。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

当物理内存不足时,内核按层次触发回收:

  1. 快速路径:分配内存时若空闲页充足,直接分配,不回收。
  2. kswapd后台回收:空闲页低于低水位线(low watermark)时,内核线程kswapd被唤醒,异步扫描LRU链表,回收文件页和匿名页。
  3. 直接回收(direct reclaim):若分配时仍无法满足,进程自己进入同步回收,阻塞等待,可能导致卡顿。
  4. 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
调优提示:文件页多、匿名页少时,系统通常较“轻”,因为文件页可快速丢弃;匿名页多且zRAM压缩率低时,系统更“重”,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分级,就能理解每个参数背后的内存语义,而不是机械地调大或调小。