夜雨聆风学习资料网

ARTICLE · 1334728

8GB 的内存, 凭什么扛得住几十个 app?

8GB 的内存,
凭什么扛得住几十个 app?

—— 从零搞懂 Android 内存 · 第 1 篇:先认识名词,再看全景

全系列 6 篇 + 番外,从零开始,逐篇只加 2~3 个新名词

五个每天都在发生、却没几个人答得上的怪事

① 8GB 的设备,free 一看只剩几百 MB,照样流畅开新 app——内存去哪了?

② 每个 app 的地址空间动辄几个 GB,几十个 app 加起来远超 8GB,系统稳如老狗;

③ 一张 12MB 的照片,相册、编辑器、显示合成器三处都在"用",物理内存只占一份;

④ 一个被杀掉的后台 app,再点开秒回原界面——它死而复生了?

⑤ 物理内存明明还剩 1GB 多,系统已经开始杀后台了。

这篇不假设你会任何内存知识。读完你会得到两样东西:一套能听懂后续所有文章的名词表,和一张六层全景地图。五个怪事,看完自然明白。

一句话版本:把内存想成一座 8GB 的公寓——CPU 手里只有门牌号(虚拟地址),门口坐着一位翻译官(MMU),拿着每户一本的字典(页表),把门牌号翻成真实房间号;房间按 4KB 一间统一切好(页);Android 又让所有租客共用公共设施(zygote、dmabuf);房不够时,还有三道防线兜底。

▲ 插图 1 | 全文的比喻:一栋只有 8GB 的分层公寓,每层只干一件事

本楼总容量 8GB,住着几十个 app 租客

01第一课:先补六个名词,后面全靠它们

网上讲内存的文章,默认你已经知道 MMU、页表是什么——这篇不默认。六个名词,每个都只给一句定义 + 一个比喻:

① 物理内存与地址

内存条(DDR)是一排排能存 0/1 的小格子,每个格子叫一个字节(Byte),每个格子有个编号,这就是地址。8GB 大约等于 80 亿个格子。CPU 想读写内存,必须报出编号。

② 虚拟地址 vs 物理地址

程序里写的、调试器里看到的地址,全是虚拟地址——相当于"门牌号";真正小格子的编号叫物理地址——"房间号"。两个世界之间隔着一个翻译官,CPU 从来不知道房间号是多少。

③ MMU:翻译官

MMU(Memory Management Unit,内存管理单元)是 CPU 内部的硬件,只干一件事:把虚拟地址查表翻译成物理地址。你可以把它当成公寓前台,CPU 每次找房间都要经过它。

④ 页表:每户一本字典

页表就是"门牌号 → 房间号"的对照表,存在内存里,每个进程一本;进程切换时,前台换一本新的。MMU 就是照着它翻译的。

⑤ 页:4KB 一个标准间

内核不按字节管内存,而是按 4KB 一块切好,每块叫一页(Page)。分配、翻译、回收都以页为单位——就像公寓统一盖成 4KB 一间的标准间,大件货住多间、打通。

⑥ TLB 与缺页中断

TLB 是 MMU 门口贴的便签:刚翻过的结果抄在上面,下次直接看,不用再翻字典。缺页中断是字典里查不到这一条时(房间还没分、或已被挪走),MMU 报错,CPU 暂停,内核现场把房间补上——这就是"缺页"。它不是故障,是正常流程。

▲ 插图 2 | 一次内存访问的翻译链:CPU 报门牌号 → MMU 查页表(先看 TLB 便签)→ 得到房间号;查不到就触发缺页中断

02带着名词看怪事:先给答案

① free 少 ≠ 内存浪费,大头当了文件缓存(page cache),随时能腾

② 几个 GB 是门牌号本(虚拟地址),不是真房间

③ 两张页表可以指向同一个物理页——一份内存多处用

④ 被杀的只是进程,界面状态还在,重新拉起再复原

⑤ 杀进程的触发条件是"内存压力",不是"还剩多少"

下面的六层地图会把它们逐个接住;详细的"为什么",每层各有一篇。

03六层全景:谁管什么,一眼可见

▲ 插图 3 | 六层全景:灰色带 = Linux 通用机制,绿色带 = Android 特化;右侧标注各层"破解的怪事"与详解篇目

从下往上看,是"一个字节怎么被存";从上往下看,是"一个 app 的内存怎么被管"。六层各解决一个问题:

L5 调度防线层 · 房不够了怎么办——丢缓存、压缩、杀后台(本篇 07 节);

L4 Android 框架层 · 怎么省着用——共享底座 zygote、Java 堆、跨进程共享的 dmabuf(本篇 06 节);

L3 进程层 · 每家一本账——进程地址空间与 VMA(本篇 05 节);

L2 虚拟地址层 · 名词课的主角——MMU、页表、TLB、缺页(本篇 04 节);

L1 内核切页层 · 地皮怎么分——4KB 页、buddy、slab、page cache(本篇 04 节);

L0 物理层 · 一块真实的地皮——DDR 的容量和带宽是硬约束。

用大白话说:以后分析内存,先问自己"我现在站在哪一层看"。free 很少是 L1 的视角,PSS 很大是 L3 的视角,buffer 没被映射卡在 L2/L3 的夹缝——层没站对,数字必然打架。

04L2/L1:一个房间号,怎么变成真实房间

如果每个进程直接用物理地址,三件惨案立刻发生:

互踩——进程 A 写错一个地址,直接抹掉进程 B 的数据,连凶手都找不到;

没地方坐——内存碎片化后,总量够却找不到一段"又大又连续"的空位;

不能共享——同一份库代码,每个进程都得复制一份,内存按进程数翻倍烧。

虚拟内存用"门牌号 + 翻译"一次解决三案:进程只拿虚拟地址,真房间由 MMU 查自己的页表翻译。于是:

隔离:每进程一本页表,A 翻不到 B 的房间——怪事② 的一半答案;

灵活:虚拟上连续的两页,物理上可以隔很远,碎片化绕开;

共享:两张页表指向同一个物理页——怪事③ 就靠这个;

超额:门牌号先发、房间用时再分(缺页中断现场补),所以"承诺"可以远大于物理——这叫 overcommit。

▲ 插图 4 | 两个进程拿着不同的门牌号,却被翻译到同一个房间——隔离与共享,一张表全办了

那 L1 在干什么?它管的是"真房间"这一侧:把 DDR 切成 4KB 的页,用 buddy 分配空闲页(按 1、2、4、8 页的大小分级挂链,尽量保住大块连续内存),用 slab 管理内核自己的小对象(几十字节的结构体按类型预铺货架),用 page cache 缓存读过的文件——怪事① 的答案就在 cache:free 少,是因为大量内存当了缓存,随时能腾。

用大白话说:判断"还能开多少 app",看 MemAvailable(空闲 + 可快速回收),不要看 free——free 只是"此刻没人用的床",cache 睡在上面但随时能叫醒。

▸ 本层详解 → 篇 2(MMU/页表/TLB/缺页)、篇 3(页/buddy/slab/page cache)。

05L3:每家一本门牌簿——VMA 与三本账

一个进程的内存不是随便摊着的,而是一串映射区间,术语叫 VMA(Virtual Memory Area)——每一行声明"哪段虚拟地址、什么权限、映射到什么":

代码段(.so 映射)——映射到文件页,只读,天然多进程共享;

私有匿名区([anon:libc_malloc])——malloc 的堆和全局变量,进程私有;

共享映射(/dmabuf:xxx、ashmem)——图形 buffer、共享内存,多人共用;

栈([stack])——每线程一段,自动伸缩。

这些区间连同"实际驻留多少"记在 /proc/PID/smaps 里。因为共享页的存在,一个进程的内存永远有三个数:

RSS(实际驻留)——共享页全额算,几个进程就重复计几次;

PSS(按比例分摊)——共享页按人数均摊,谈"这个进程占多少"默认用它;

USS(纯私有)——杀掉它立刻能回收的部分。

这一层实战篇第 1 篇已完整拆过(camera.provider 的 747MB),本系列直接引用结论。

▸ 本层详解 → 篇 4(VMA 逐段、smaps 字段、三本账互证)+ 实战三篇。

06L4:公共大堂与共享储物柜

通用 Linux 只给房间,Android 又加了四个"公共设施",专门为了省内存:

zygote:所有 app 的样板房

系统启动时先装修好一间样板房,装满常用类库和资源;之后每个 app 都用 fork(复制进程)从它分出来。COW(写时复制)的意思是:谁都不改动,就共享同一批页;某个 app 改了哪页,才复制哪页。所以"hello world 也占几十 MB"里,大头其实是和别人共享的。

ART:Java 堆的物业

Java 对象住 ART 管的大堆里,由 GC(垃圾回收)自动清理。它底层仍是普通的匿名页,只是多了自动管理;Native Heap(C/C++ malloc)则没人兜底,泄漏就是真泄漏。

ashmem / memfd:共享储物间

让两个进程共享一段内存的机制。传递方式是 binder 传 fd(文件描述符;可以理解为"房间钥匙的编号"),钥匙传过去,两边开的同一扇门。

dmabuf / ion / gralloc:硬件共享集装箱

相机、GPU、显示这些硬件要的内存,必须物理连续、带缓存属性、还能跨设备传——内核统一用 dma-buf 承载,ion/dma-heap 负责分配,gralloc 把它包成 GraphicBuffer。怪事③ 的另一半在这,实战篇 747MB 的 84% 就是它。

▲ 插图 5 | 三个 app 共享同一个底座,只有自己改过的页才复制——这就是 COW

L4 的思路一句话:能共享的绝不复制,能缓存的绝不重算,能延迟的绝不预占。

▸ 本层详解 → 篇 5(zygote/COW、ART 堆、ashmem/memfd、dmabuf 全链路)。

07L5:旺季腾房三招——回收、压缩、杀

如果共享和延迟都用上了还不够,系统按"代价从低到高"依次出三招:

▲ 插图 6 | 三道防线:丢缓存 → zRAM 压缩 → LMKD 杀进程;触发器是 PSI 内存压力,不是剩余量

两个名词在图里出现了,顺手定义:

PSI(Pressure Stall Information)——内核统计的"内存压力体温计":最近有多少时间,进程因为等内存而卡住了。数值高说明大家已经开始排队,不是"还剩多少 GB"能反映的;

LMKD(Low Memory Killer Daemon)——用户态的"低内存杀手",盯着 PSI,按 oom_adj(进程重要性打分,越不重要分越高)从后台开始杀;

zRAM——内存里划出一块当"压缩盘":把不活跃的匿名页压缩后塞回去,拿 CPU 换内存。

于是怪事④⑤ 都顺了:⑤ 因为触发看 PSI 不看剩余;④ 因为被杀的是进程,Activity 栈和界面状态还在 system_server 手里,重新拉起后按保存状态复原,快得像没死过。

▸ 本层详解 → 篇 6:水位线、kswapd/direct reclaim、zRAM 压缩比、oom_adj 分值表。

08收个尾:8GB 全程对一次账

名词和地图都有了,看一眼真实设备的账本(数值为示意):

▲ 插图 7 | 8GB 账本(示意):匿名页和 page cache 是大头,free 只是零头

两条命令拿到这些数:

# L1 视角:内核手上的物理内存总账
adb shell cat /proc/meminfo

# L3/L4 视角:按进程摊的 PSS 账
adb shell dumpsys meminfo

MemTotal 7.3G —— L0:物理总量(扣除内核保留)

Cached 2.8G —— L1:page cache,可回收

AnonPages 2.9G —— L3/L4:进程私有页 + Java 堆

Slab 350M / PageTables 45M —— L1:内核自己花的

MemAvailable 2.5G —— 判断"还能开多少 app"的唯一口径

对账三步:① meminfo 看 L1 总盘子;② dumpsys meminfo 把 AnonPages 按 PSS 摊到各进程(L3/L4);③ 碰上图形大户走 bufinfo 查 dmabuf(L4)。三步走完,8GB 没有黑箱。

09动手作业 + 后面的路怎么走

五条命令,每条对应一层,先跑一遍截图存好——后面几篇会拿你自己的数字讲:

# L1:物理端总账(page cache / slab / MemAvailable)
adb shell cat /proc/meminfo | head -5

# L2/L3:任选一个进程,看它的 VMA 账本
adb shell su -c "cat /proc/$(pidof system_server)/smaps" | head -20

# L4:全系统 dmabuf 登记表(图形共享的大盘子)
adb shell su -c "ls -l /sys/kernel/debug/dma_buf/bufinfo"

# L5:内存压力(PSI)—— LMKD 的触发依据
adb shell cat /proc/pressure/memory

# L5:zRAM 压缩了多少
adb shell cat /sys/block/zram0/mm_stat

学习路线:每篇只加 2~3 个新名词

篇 1 · 入门与全景(本篇)|前置:无 | 收获:六个基础名词 + 六层地图 + 五个怪事的答案;

篇 2 · MMU、页表与缺页|前置:篇 1 | 一次读写到底怎么翻地址、页表长什么样、缺页的两种结局;

篇 3 · 物理内存管理|前置:篇 1 | 页、buddy、slab、page cache 的内部结构;

篇 4 · 进程账本|前置:篇 1 | VMA、smaps、RSS/PSS/USS,把账算准;

篇 5 · 共享内存与图形|前置:篇 1、4 | zygote/COW、ART、ashmem、dmabuf;

篇 6 · 低内存防线|前置:篇 1、3 | 回收、zRAM、PSI、LMKD;

番外 · 车机多 VM|前置:篇 1、5 | hypervisor 下 8GB 怎么分给两个系统。

建议节奏:每篇约 20 分钟 + 跑一遍命令 + 答完文末自测题,再进下一篇。

自测三问(答不出就回头再看 01 节)

1. 虚拟地址和物理地址有什么区别?为什么要两套?
答:门牌号 vs 房间号;为了隔离、灵活、共享、超额承诺。

2. MMU 的全部工作是什么?查不到会怎样?
答:查页表把虚拟地址翻成物理地址;查不到触发缺页中断,内核补。

3. 为什么 free 很少不代表内存浪费?
答:大量内存当了 page cache,可回收;看 MemAvailable。

本篇术语卡(收藏向)

物理内存 存 0/1 的格子 / 地址 格子编号 / 虚拟地址 程序用的门牌号 / 物理地址 真实房间号

MMU 翻译硬件 / 页表 门牌→房间的字典 / TLB 翻译结果便签 / 缺页中断 查不到时内核补页

页 4KB 管理单位 / page cache 文件缓存 / VMA 映射区间 / RSS·PSS·USS 三本账

zygote·COW 共享底座·写时复制 / dmabuf 硬件共享集装箱 / PSI 压力指标 / LMKD 低内存杀手

下一篇(篇 2)只干一件事:跟着一次普通的内存读写,走完"CPU 报地址 → MMU 查页表 → 拿到物理页"的完整链路,把今天这张翻译链图上的每个箭头都拆开讲透。

「从零搞懂 Android 内存」系列 1/6 · 图中数值为示意,实机请自行核对 · 实战前作:《读懂 Android 进程内存》三篇

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料