fork与COW
Zygote 的内存收益来自 Linux fork 的 copy-on-write(COW),但“fork 后完全共享内存”是错误说法。fork 先复制进程的虚拟地址空间和页表关系,父子进程把可写页映射为 COW,共享同一物理页;任一方第一次写入时,内核才复制对应页。Zygote 预加载越稳定,子进程越能长期共享;specialization、Application 初始化、GC 和缓存写入会逐步制造私有页。
1. Java fork入口
源码文件:frameworks/base/core/java/com/android/internal/os/Zygote.java
static int forkAndSpecialize(
int uid, int gid, int[] gids,
int runtimeFlags, int[][] rlimits,
int mountExternal, String seInfo,
String niceName, int[] fdsToClose,
int[] fdsToIgnore, boolean startChildZygote,
String instructionSet, String appDataDir,
boolean isTopApp, ... ) {
ZygoteHooks.preFork();
int pid = nativeForkAndSpecialize(...);
if (pid == 0 && gids != null && gids.length > 0) {
NetworkUtilsInternal.setAllowNetworkingForProcess(
containsInetGid(gids));
}
Thread.currentThread().setPriority(
Thread.NORM_PRIORITY);
ZygoteHooks.postForkCommon();
return pid;
}preFork() 停止 ART daemon、等待 native unregister;native fork 返回后,子进程更新网络允许状态,父子都恢复 Java 优先级并执行 postForkCommon()。COW 在 native fork 返回的一刻已经建立,post-fork hook 是运行时恢复,不是触发 COW 的开关。
2. ForkCommon准备
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
pid_t zygote::ForkCommon(
JNIEnv* env, bool is_system_server,
const std::vector<int>& fds_to_close,
const std::vector<int>& fds_to_ignore,
bool is_priority_fork,
bool is_top_app,
bool use_fifo_ui,
bool purge) {
SetSignalHandlers();
BlockSignal(SIGCHLD, fail_fn);
// 检查/关闭继承 fd,准备运行时和 profiling 状态。
...
pid_t pid = fork();
...
}ForkCommon 在 Zygote 单线程假设下阻塞 SIGCHLD,避免 fork 期间 handler 重开日志 fd;它还处理 fd allowlist、优先级和 runtime hooks。fork 前任何后台线程或不受控 fd 都可能导致父子状态不一致,因此这些准备是正确性条件,不只是性能优化。
3. COW的页级语义
fork 后父子拥有独立页表,但页表项指向相同物理页并被标记为只读/COW。读操作继续共享;写操作触发 page fault,内核为写入方分配新页并复制原内容,然后更新写入方页表。复制粒度是内存页,不是 Java 对象,也不是整个堆。
4. 预加载为何有效
SS006~SS008 的类、dex cache、resource ConstantState、shared library 和字体缓存都在父 Zygote 中建立。fork 后子进程读取这些对象时可共享物理页;如果每个应用都在 fork 后重复初始化,它们会分别分配/写入私有页。预加载是否划算取决于“多少子进程会读”和“对象是否稳定不写”。
5. 进程特化写入
源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
static void SpecializeCommon(
JNIEnv* env, uid_t uid, gid_t gid,
jintArray gids, jint runtime_flags,
... ) {
EnableKeepCapabilities(fail_fn);
SetInheritable(permitted_capabilities, fail_fn);
DropCapabilitiesBoundingSet(fail_fn,
bounding_capabilities);
MountEmulatedStorage(uid, mount_external,
need_pre_initialize_native_bridge, fail_fn);
ensureInAppMountNamespace(fail_fn);
...
SetCapabilities(...);
selinux_android_setcontext(
uid, is_system_server,
se_info_ptr, nice_name_ptr);
}specialization 改变内核进程状态、mount namespace、capability、SELinux 和 runtime 配置。部分动作不复制 Java heap 页,但会写入 libc/ART/runtime bookkeeping,从而产生 COW。应用后续 ActivityThread、ClassLoader、Application 和缓存初始化会继续私有化更多页。
6. 哪些内存继续共享
- 只读 boot image、odex/vdex、共享库文件映射通常通过文件页共享,不依赖匿名 COW;
- Zygote 预加载的匿名堆页在未写时 COW 共享;
- Binder driver buffer、线程栈、应用堆新分配和 per-process native heap 是子进程私有;
MAP_SHARED文件/ashmem 的写入可在进程间可见,与匿名 COW 不同;- GC 修改 mark bitmap、对象头或移动对象时可能造成额外私有页。
7. 父Zygote写入成本
父 Zygote 在 fork 后若修改预加载对象所在页,会让自己获得私有副本;已有子进程仍指向原页,后续新子进程则继承父进程当前页。频繁修改全局缓存不仅增加父进程 PSS,还会让不同时刻 fork 的应用看到不同共享基线。因此 Zygote 常驻循环尽量避免写预加载对象。
8. 观测与误读
PSS 会按共享者分摊共享页,USS 只统计进程私有页;一个应用 PSS 增长可能来自共享者退出导致分摊变化,而非实际新分配。分析 COW 应对比 private dirty、shared clean/dirty、匿名页和文件映射,并在相同进程集合下采样。
# Java/native fork 与 post-fork hooks。
rg -n "forkAndSpecialize|preFork|nativeForkAndSpecialize|postForkCommon" \
frameworks/base/core/java/com/android/internal/os/Zygote.java \
libcore/dalvik/src/main/java/dalvik/system/ZygoteHooks.java
# native ForkCommon、fd、signal 和 specialization。
rg -n "ForkCommon|BlockSignal|fork\(|SpecializeCommon|MountEmulatedStorage|selinux_android_setcontext" \
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
# 运行时采样:比较同一进程在初始化前后的 PSS/USS/private dirty。
adb shell dumpsys meminfo <pid>
adb shell cat /proc/<pid>/smaps_rollup9. 失败与设计边界
- fork 前线程未停:可能复制不一致的用户态锁/运行时状态;
- fd 未关闭:子进程继承 Zygote 控制面或敏感资源;
- specialization 失败:子进程终止,不能回到未特化状态继续;
- 过度预加载:所有子进程承担不常用页的页表/PSS 成本;
- fork 后修改预加载缓存:增加 COW private dirty;
- 把 COW 称为一次内存拷贝:忽略页级、按写触发和文件映射共享。
下一篇将进入 RuntimeInit.applicationInit() 与应用 Java main 的反射入口;不会重复 fork/COW 和 specialization。
