Zygote 与写时复制:进程为何能共享内存
Zygote 与写时复制:进程为何能共享内存
上一章看了分组记账。这一章回答:应用进程怎么被创建,为什么创建很快、内存很省, 预加载的类与资源是怎么被共享的?
Zygote 在 fork 前完成公共类和资源的 preload。子进程最初继承相同虚拟地址映射,物理页在 未写入时保持共享;写时复制减少的是重复物理页,不代表每个进程的 PSS 会完全相同。
fork 继承地址空间,不立即复制物理页
| 常见误解 | 正确理解 |
|---|---|
| 每个应用进程都要重新加载系统类 | Zygote 预加载一次,子进程通过 fork 继承共享内存页 |
| fork 会复制父进程全部内存 | 写时复制:先共享,只有写入时才复制页 |
| 应用启动慢是因为 fork 慢 | fork 很快,慢在应用自己的初始化与 dexopt 产物加载 |
核心结论
- 应用进程由 Zygote fork 而来,Zygote 预加载框架类与通用资源。
- fork + CoW 让预加载内存页在多个应用间共享,物理内存只存一份。
- 应用写入后触发页复制,私有内存随使用增长。
- 应用自己的 dex 加载、JIT/ART 产物仍是每进程一份,启动成本主要在这里。
- 内存视角下“Zygote 内存大”不可怕,共享页占比高反而说明省内存。
fork 与共享主链
逐步解释:
- 预加载:Zygote 启动时预加载框架类与资源。
- fork:AMS 请求 Zygote 创建子进程,走 forkAndSpecialize。
- 共享:子进程继承预加载页,未写入前物理共享。
- 独立:应用初始化与业务写入逐步产生私有页。
为什么“Zygote 占内存多”不一定是坏事
用 dumpsys meminfo 看 Zygote 时数值可能很大,但其中大量是共享页。共享页越多,说明 应用之间复用越多,整体越省内存。判断内存问题时,要区分 PSS(按共享比例分摊)与 RSS (全算自己),不能只看 RSS。
源码证据
阅读目标:确认 fork 入口与预加载。
正文指针:Zygote.java forkAndSpecialize();ZygoteInit.java preload(); Native 侧 app_main.cpp 与 com_android_internal_os_Zygote.cpp。
// ZygoteInit.java(伪代码,行号以 r75 为准)
static void preload() {
preloadClasses(); // 预加载框架类
preloadResources(); // 预加载通用资源
}这段代码证明: 预加载发生在 Zygote 启动早期;fork 之后这些内存页被子进程继承。 这也是 Android 应用进程比传统 fork 场景“更省内存”的根源。
验证与排障
环境:开发设备或模拟器;以下命令不需要 root。
adb shell ps -A | grep -E "zygote|app_process"
adb shell dumpsys meminfo <包名>
adb shell cat /proc/<pid>/status | grep -E "VmRSS|VmSize"ps 确认进程父进程是 zygote;dumpsys meminfo 看 PSS/RSS;/proc/<pid>/status 看 虚拟与物理内存。排查“进程内存暴涨”时,先区分共享页与私有页,再定位私有增长来源。
常见误区
- “每个应用都重新加载框架类”:预加载一次,fork 共享。
- “fork 会复制全部内存”:写时复制,写才复制页。
- “应用启动慢是 fork 慢”:fork 很快,慢在应用初始化。
- “Zygote 内存大 = 浪费”:共享页占比高反而是优势。
延伸问题
- PSS、RSS、USS 的区别与内存排查的适用场景是什么?
- fork 后哪些内存一定会复制(如写时复制不生效的情况)?
- 应用 dex 加载为什么不能完全共享?
- 厂商预加载自定义类对启动与内存有什么影响?
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
Zygote.java | forkAndSpecialize() | 子进程创建 |
ZygoteInit.java | preload() | 类与资源预加载 |
com_android_internal_os_Zygote.cpp | Native fork | 实际 fork 调用 |
/proc/<pid>/status | VmRSS | 内存观测 |
公共路径:frameworks/base/core/java/com/android/internal/os/ 与 frameworks/base/core/jni/。行号以 r75 检索为准。
应用私有 Java 对象如何分配与回收,见 ART 堆与 GC。