1、内存管理全景:Android内存子系统架构概览,从应用层到内核层的完整链路
各位好,我是老张。今天咱们正式开始聊Android内核的内存回收机制。说实话,这个主题我准备了很久,因为它确实复杂——从你手指滑动屏幕,到内核悄悄回收一个内存页,中间跨越了整整五层软件栈。
我个人习惯,讲任何系统之前,先画一张全景图。就像你去一个陌生城市,先看地图一样。今天这第一讲,就是带你把Android内存管理的“地图”看明白。
1.1 为什么需要理解内存子系统架构?
你可能觉得,内存管理不就是GC(垃圾回收)那点事吗?嗯,我刚开始也这么想。直到有一次,我在项目里遇到一个诡异的内存泄漏——应用层怎么查都查不出问题,最后一路追到内核,才发现是驱动层的一个page cache没释放。
那次之后我明白了:Android的内存问题,从来不是某一层的事。从你写的Java代码,到C++的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泄漏的根因。
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在界面上显示一张图片:
- 应用层:调用BitmapFactory.decodeResource(),ART在Java Heap中分配一个Bitmap对象。
- Native层:Bitmap的像素数据通过Skia引擎,在Native层通过mmap分配匿名内存。
- 内核层:mmap触发缺页异常,内核的页分配器从buddy system中拿出一页物理内存。
- 如果内存紧张:kswapd开始扫描,发现这个页面最近没被访问,把它压缩到zRAM里。
- 如果还紧张:LMK检查进程优先级,如果App在后台,直接杀掉。
你看,一个简单的图片加载,背后是五层系统的协同工作。哪一层出问题,都会导致内存异常。
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
这些命令,后面都会用到。现在先记下来,后面实操的时候会很有帮助。
好了,第一讲就到这里。下一讲,我们会深入内核的页分配器,看看buddy system到底是怎么工作的。到时候见。