Zygote: the mother of processes
Every app and system_server is a fork of one warm process. Zygote is why an app can start in a few hundred milliseconds despite having no main() of its own.
A desktop program starts with main(). An Android app does not. There is no entry point in your
code that the kernel calls, because the process that will run your app is created by copying another
process that has already done most of the work.
TL;DR#
- Zygote is a process started by
initat boot. It preloads the framework classes and resources, then waits for requests. - Every app process, and
system_serveritself, is created by forking Zygote. The child inherits the preloaded state through copy-on-write pages, so it skips class loading. - The parent does not exec a new program. That is the whole trick:
forkwithoutexec. - A second, faster path exists: the USAP pool, a set of pre-forked children kept ready so the system can specialise one instead of forking on the critical path.
- A separate app zygote serves isolated processes such as WebView renderers, which need a different security context.1
Mental model#
Imagine a bakery that sells a hundred kinds of cake.
Every morning the bakers prepare a base sponge: flour, eggs, sugar, the expensive part of the work. When a customer orders, the baker takes a portion of the base, adds the flavouring, and puts it in the oven. Nobody starts from flour for a single slice.
The base sponge is Zygote's preloaded state. The portion is the forked child. The flavouring is the app's own classes, resources and process identity.
Where the analogy breaks#
A baker takes a piece of the base. Zygote's child initially shares every page with the parent; the pages are duplicated only when one side writes to them. That is why fifty running apps do not cost fifty copies of the framework — until each app starts modifying those pages, which it does almost immediately in its own heap but rarely in the shared class metadata.
The big picture#
Text description of this diagram
A fork tree with four moments. init starts zygote64. Zygote preloads the boot classpath and the preloaded-classes list. system_server is forked from Zygote. App A and App B are forked later, each inheriting the shared class pages. A USAP from the pool is specialised into App C. The strip under the tree shows the copy-on-write map: shared pages stay shared, and private pages grow as each child writes to its own heap.
Step through the four moments and watch the tree grow. The important thing to notice is what does
not happen: there is no moment where an app process loads Activity, View or the Kotlin
standard library. Those were loaded once, before any app existed.
Deep dive#
Boot: who starts Zygote#
init reads its .rc files and starts services. Zygote is one of them: the app_process binary
with com.android.internal.os.ZygoteInit as its entry class. On a 64-bit device the process is
usually zygote64; a 32-bit Zygote may also run to serve 32-bit apps.1
ZygoteInit does four things, in order:
- Create the socket the system server will use to request processes.
- Preload the boot classpath and the classes listed in the preloaded-classes file.
- Preload resources, so that common drawables and strings are already in memory.
- Enter the fork loop, waiting for commands on the socket.
The preloaded set is a deliberate trade-off. Loading more classes makes every fork warmer; it also makes Zygote itself slower to start and uses memory before any app exists. The list is tuned per release, and the boot image profile influences which classes get compiled rather than merely loaded.2
The fork itself#
When you tap an icon, system_server resolves the launch to a process name and asks Zygote for a
process. The request travels over a Unix domain socket — not Binder, because the child does not exist
yet and has no Binder handle.
Zygote calls fork(). In the child:
- The PID is new; the parent's PID is the old one.
- The UID is still Zygote's until the child calls
setuidto adopt the app's UID. - The address space is a copy-on-write clone of the parent's.
- The child closes the socket it inherited and starts the
ActivityThreadmain loop.
The parent returns to the fork loop and waits for the next request. It never execs, never restarts, and never accumulates app state.
In code, the shape is roughly this. The real implementation has more steps — SELinux context, cgroups, mount namespaces — but the sequence is the same:
// frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java (shape, not verbatim)
int pid = Zygote.forkAndSpecialize(uid, gid, gids, runtimeFlags, rlimits,
mountExternal, seInfo, niceName, fdsToClose, ...);
if (pid == 0) {
// child: adopt identity, then run the app's main loop
handleChildProc(parsedArgs, descriptors, childPipeFd, newPeer);
return true; // never reached
} else {
// parent: report the pid back to system_server, keep serving
handleParentProc(pid, descriptors, peer);
return false;
}The if (pid == 0) branch is the moment the two processes diverge. Both are executing the same
function at the same instruction; one will return to the fork loop, the other will become your app.
File descriptors and hygiene#
A forked child inherits the parent's open file descriptors. If Zygote has a file open when it forks, the child gets it too, and that can leak access or hold a resource open longer than intended.
Zygote enforces descriptor hygiene across forks: descriptors that are not on an allowlist are
redirected to /dev/null at fork time, so a cached descriptor cannot accidentally reach a file the
child did not open.1 This is a small detail with a large security
consequence, and it is the reason framework changes that open files in Zygote produce SELinux denials
and I/O failures rather than silent leaks.
The USAP pool#
Forking is fast, but it is not free: the child still has to be scheduled, adopt its identity, and start the runtime. The unspecialised app process pool removes part of that from the critical path.
With USAP enabled, Zygote keeps a set of already-forked children that have completed the application-independent parts of startup. When the system needs a process, it asks a pooled USAP to specialise itself — change PID, cgroup and related state — and the USAP reports back. Once used, it leaves the pool, and Zygote refills the pool when it has one or fewer left.1
The pool is a latency optimisation, not a correctness feature. If it is empty, the system falls back to forking.
More than one Zygote#
There is not one Zygote. There are at least two roles:
- The main Zygote forks ordinary apps and
system_server. - The app zygote forks isolated processes — the kind used for WebView renderers and other untrusted code — which need a different security context and a leaner class set.1
There can also be a 32-bit Zygote alongside the 64-bit one on devices that support both ABIs. Each Zygote has its own preloaded state, so the "one warm template" story is really "one warm template per ABI and per security context".
What Zygote is not#
Zygote is not a supervisor and not a scheduler. It does not decide which app to start, how important
it is, or when to kill it. That is system_server's job, through the activity manager and the
low-memory killer.
It is also not a security boundary in the sense of enforcing app permissions. The child adopts its own UID, and SELinux confines it to its own domain. Zygote's role is speed, with a side of consistency: every app starts from the same known state.
That distinction matters when something goes wrong. If an app is slow to start, the question is
whether Zygote was slow to fork, whether the child was slow to specialise, or whether the app's own
Application.onCreate is the bottleneck. Zygote only owns the first two, and the third is usually
the answer.
What the child inherits, and what it does not#
The copy-on-write story is easy to state and easy to overstate. A forked app inherits:
- Loaded classes and their metadata — the bulk of the win.
- The boot classpath and everything Zygote preloaded.
- Preloaded resources — drawables, strings and other common assets.
- The runtime's initialized state — heap, thread registry, JIT state.
It does not inherit:
- The app's own classes. Those are loaded from the APK after the fork, from the app's class loader.
- The app's UID or permissions. Those are set after the fork.
- Any state from a previous instance of the same app. Each launch is a fresh fork.
The last point is why an app's process is not "warm" in the way a browser tab is. Android does keep processes alive, so a relaunch may reuse an existing process — but that is the activity manager's doing, not Zygote's.
How it evolved#
| API | Version | Released | What changed |
|---|---|---|---|
| 1 | 1.0 | 2008-09-23 | — |
| 19 | 4.4 KitKat | 2013-10-31 | — |
| 21 | 5.0 Lollipop | 2014-11-03 | — |
| 24 | 7.0 Nougat | 2016-08-22 | — |
| 29 | 10 | 2019-09-03 | — |
| 30 | 11 | 2020-09-08 | — |
| 37 | 17 Cinnamon Bun | 2026-06-16 | — |
Zygote's design has been remarkably stable. What changed around it is how fast the handoff is (USAP), what the runtime does with the inherited image (ART's boot image profiles), and how the system treats processes after they start (freezing, memory limits).
Under the hood#
- ZygoteInit.javapreload, socket, fork loop
- Zygote.javathe fork primitive and its post-fork steps
- fd_utils.cppthe descriptor allowlist
- preloaded-classesthe tuned list of classes loaded before any app
- boot-image-profile.txtwhich boot-classpath methods get compiled
Curiosity corner#
- An app inherits its parent's open files. That is why Zygote has an explicit descriptor allowlist rather than trusting fork to be clean.
- The USAP pool is refilled lazily. Zygote tops it up when it drops to one or fewer, not on a timer.
system_serveris an app process too. It is forked from Zygote the same way, which is why the framework's own startup benefits from the preloaded state.
Try it#
adb shell ps -A | grep zygotePIDs and memory figures are device-specific; the names are the stable part.
adb shell ps -A -o USER,PID,PPID,NAME | head -20PIDs and UIDs are device-specific. What matters is the PPID column: app processes and system_server share a parent.
Check yourself#
Check yourself
How is a new app process created from Zygote?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsZygotesource.android.comaccessed 2026-10-27↩
- docsAndroid Runtime (ART) and Dalviksource.android.comaccessed 2026-10-27↩
- docsAndroid platform architecture (AOSP)source.android.comaccessed 2026-10-27↩
- docsProcesses and threads overview | App qualitydeveloper.android.comaccessed 2026-10-27↩