1、内存管理全景:Android内存子系统架构概览,从应用层到内核层的完整链路

各位好,我是老张。今天咱们正式开始聊Android内核的内存回收机制。说实话,这个主题我准备了很久,因为它确实复杂——从你手指滑动屏幕,到内核悄悄回收一个内存页,中间跨越了整整五层软件栈。

我个人习惯,讲任何系统之前,先画一张全景图。就像你去一个陌生城市,先看地图一样。今天这第一讲,就是带你把Android内存管理的“地图”看明白。

1.1 为什么需要理解内存子系统架构?

你可能觉得,内存管理不就是GC(垃圾回收)那点事吗?嗯,我刚开始也这么想。直到有一次,我在项目里遇到一个诡异的内存泄漏——应用层怎么查都查不出问题,最后一路追到内核,才发现是驱动层的一个page cache没释放。

那次之后我明白了:Android的内存问题,从来不是某一层的事。从你写的Java代码,到C++的Native堆,再到内核的页分配器,每一层都可能成为瓶颈。

核心观点: Android内存管理是一个分层协作的系统。应用层负责“怎么用”,内核层负责“怎么给”。中间还有一层Native运行时,负责“怎么管”。

1.2 整体架构:五层模型

我把Android内存子系统从上到下分成五层。你想想看,是不是这个理?

层级 名称 主要职责 典型组件
第1层 应用层 内存分配与释放请求 Java Heap、Bitmap、Dalvik/ART
第2层 Framework层 进程优先级与内存策略 AMS、LMK、OOM Adjuster
第3层 Native层 C/C++堆管理 jemalloc、mmap、ashmem
第4层 内核通用层 物理页管理与虚拟内存 页分配器、slab、VMA
第5层 内核回收层 内存回收与压缩 kswapd、LMK、zRAM、CMA

这五层不是孤立的。我举个例子:你在应用层new一个Bitmap,ART会向内核申请一块匿名内存页。如果内存紧张,内核的kswapd线程会尝试回收不常用的页面。如果还不够,Framework层的LMK(低内存杀手)就会开始杀进程。你看,一个操作串起了三层。

3 每一层在做什么?

1.2.1 应用层:内存的“消费者”

应用层是内存的起点。说白了,你的App就是那个“伸手要内存”的人。Java代码里new出来的对象,都放在ART虚拟机管理的Java Heap里。

这里有个坑,我踩过:Bitmap的内存并不全在Java Heap里。Android 3.0之后,Bitmap的像素数据放在了Native层。你如果只盯着Java Heap看内存,永远找不到Bitmap泄漏的根因。

避坑指南: 我曾经花了两天时间排查一个内存泄漏,最后发现是Bitmap.recycle()没调用。虽然现在有自动回收,但大图场景下,手动释放仍然是最可靠的。

1.2.2 Framework层:内存的“调度员”

Framework层干的事,说白了就是“谁该活,谁该死”。它通过AMS(ActivityManagerService)给每个进程分配一个oom_adj值。这个值越低,进程越重要,越不容易被杀。

你可能会问:为什么我的App在后台待了一会儿就被杀了?嗯,看看它的oom_adj值就知道了。前台App一般是0,后台Service可能是5,而空进程可能直接飙到15。值越高,越容易被LMK盯上。

// 查看进程的oom_adj值(需要root)
adb shell cat /proc/[pid]/oom_adj
// 输出示例:0  (前台进程)
// 输出示例:12 (缓存进程)

1.2.3 Native层:内存的“搬运工”

Native层是C/C++代码的天下。Android的SurfaceFlinger、MediaServer这些核心服务都跑在这一层。它们用的不是Java Heap,而是通过jemalloc或直接mmap来管理内存。

我记得有一次,一个多媒体App播放视频时内存暴涨。查到最后,发现是MediaCodec在Native层申请了连续物理内存(通过ION分配器),但释放不及时。这种问题,你在Java层根本看不到。

1.2.4 内核通用层:内存的“仓库管理员”

内核层管的是真正的物理内存。它把物理内存分成一个个4KB大小的页(page),然后通过页分配器(buddy system)来管理。你申请内存,它给你一页;你释放,它回收。

这里有个概念很重要:虚拟内存 vs 物理内存。每个进程看到的都是独立的虚拟地址空间,内核负责把虚拟地址映射到物理地址。这种映射关系,记录在页表里。

注意: 虚拟内存可以比物理内存大得多。但一旦你真正访问了某个虚拟地址(缺页异常),内核就必须给它分配物理页。这就是“按需分配”的原则。

1.2.5 内核回收层:内存的“清道夫”

这一层是我们课程的核心。当物理内存不足时,内核会启动一系列回收机制:

  • kswapd:内核的守护线程,定期检查内存水位。低于阈值就开始异步回收。
  • 直接回收:如果kswapd来不及,分配内存的进程会自己触发同步回收。
  • LMK:低内存杀手,直接杀掉优先级低的进程来释放内存。
  • zRAM:把不常用的匿名页压缩后放在内存里,用CPU换空间。
  • CMA:为摄像头等设备预留的连续内存区域,平时可以被普通进程使用,需要时再回收。

这些机制怎么配合?我画个简化的流程图:

内存分配请求
    ↓
检查空闲页是否充足?
    ├── 是 → 直接分配
    └── 否 → 唤醒kswapd
              ↓
        kswapd尝试回收:
        ├── 回收文件页(page cache)
        ├── 回收匿名页(swap/zRAM)
        └── 如果还不够 → 触发LMK杀进程

1.3 一个完整的例子:从App到内核

咱们来个实际的。假设你的App在界面上显示一张图片:

  1. 应用层:调用BitmapFactory.decodeResource(),ART在Java Heap中分配一个Bitmap对象。
  2. Native层:Bitmap的像素数据通过Skia引擎,在Native层通过mmap分配匿名内存。
  3. 内核层:mmap触发缺页异常,内核的页分配器从buddy system中拿出一页物理内存。
  4. 如果内存紧张:kswapd开始扫描,发现这个页面最近没被访问,把它压缩到zRAM里。
  5. 如果还紧张:LMK检查进程优先级,如果App在后台,直接杀掉。

你看,一个简单的图片加载,背后是五层系统的协同工作。哪一层出问题,都会导致内存异常。

总结一下: Android内存管理不是单一的技术,而是一个分层协作的系统。理解这五层架构,是深入内存回收机制的基础。后面的章节,我们会一层一层地拆开来看。

1.4 学习建议

我个人建议,学这个课程的时候,手里最好有一台root过的手机。为什么呢?因为很多内核参数,只有root了才能看。比如:

# 查看内存水位线
adb shell cat /proc/zoneinfo

# 查看kswapd活动
adb shell cat /proc/vmstat | grep pgscan

# 查看zRAM使用情况
adb shell cat /sys/block/zram0/mm_stat

这些命令,后面都会用到。现在先记下来,后面实操的时候会很有帮助。

小技巧: 如果你没有root设备,可以用Android模拟器。模拟器的内核参数和真机基本一致,而且可以随时重置,不怕搞坏。

好了,第一讲就到这里。下一讲,我们会深入内核的页分配器,看看buddy system到底是怎么工作的。到时候见。