Zygote
Zygote, Dalvik & ARTnerd

ART: ahead-of-time, then hybrid

ART began as the runtime that compiled everything at install. It is now a hybrid of interpreter, JIT and profile-guided AOT, shipped as an updatable module — and each swing of that pendulum was a reaction to the previous one's cost.

12 minAPI 19–37 · 17interactivequizverified 2026-10-27

ART's history is a sequence of corrections. Full AOT at install made apps fast and made installs slow. Removing AOT made installs fast and made first launch slow. The hybrid model — interpret, JIT, profile, recompile when idle — is the compromise that survived.

TL;DR#

  • ART replaced Dalvik. It appeared as an experimental runtime in Android 4.4 (API 19) and became the default in Android 5.0 (API 21).1
  • Android 5.0 compiled apps ahead of time at install with dex2oat. That produced fast code and long installs.
  • Android 7.0 (API 24) added a JIT plus profile-guided AOT model: install is fast, hot code is compiled from a profile, and the rest is interpreted.
  • A Baseline Profile shipped in the APK lets ART compile the hot methods at install, so first launch does not pay for JIT.
  • ART is a Mainline module — it ships as an APEX and can be updated outside an OS update.2

Mental model#

Think of a restaurant that has to decide when to prep ingredients.

Prep everything in advance (full AOT) and service is fast, but opening a new branch takes all day, and you throw away prep for dishes nobody orders.

Prep nothing (pure interpretation) and you can open in an hour, but every order is slow at first, and the kitchen only gets fast once it has cooked a dish a few times.

Prep the dishes you know will be ordered (profile-guided AOT) is the compromise, and it only works if you can predict the menu. That prediction is the profile.

Where the analogy breaks#

A restaurant's menu is fixed. An app's hot path depends on the user: one person lives in the chat screen, another in the camera. ART's answer is that the profile is collected per app, not per user — a statistical bet about what most users do, refined over time on the device.

The big picture#

Loading diagram…
Text description of this diagram

A six-stage pipeline. On the build machine, kotlinc produces class files, R8 optimises and desugars them, and d8 produces DEX. At install, dex2oat compiles the methods named in a Baseline Profile into oat and vdex files. At runtime the JIT compiles hot methods that were not precompiled and records a profile. During idle maintenance dex2oat recompiles using that profile. The diagram labels which stages happen on the build machine, which at install, and which on every run.

The important structural fact is that compilation happens three times, in three places, for three different reasons. Build-time work is paid once by the developer. Install-time work is paid once per device. Runtime work is paid on every launch, which is why it is the one ART works hardest to avoid.

Deep dive#

Becoming the default#

ART shipped in Android 4.4 as an experimental runtime that developers could enable. Android 5.0 made it the only runtime.1 The Dalvik-era contract did not change: apps ship DEX, the zygote preloads classes, and the process model is identical. What changed is what happens to the DEX.

Full AOT: the first model#

In Android 5.0, dex2oat ran at install and compiled the app's DEX to native code, stored in an .oat file next to the original bytecode in a .vdex. The result:

  • Fast execution — the code is native from the first instruction.
  • Slow installs — compiling a large app takes time, and the device is busy while it happens.
  • Wasted work — code that is never executed was compiled anyway.
  • Storage cost — the compiled output is larger than the DEX it came from.

On a phone with limited flash and a user waiting for an install to finish, all three costs were real.

The install-time model also had a sizing problem. A large app could take a noticeable fraction of a minute to install, and the work happened while the device was otherwise idle but the user was waiting. On low-end hardware, with slow flash, it was worse. Users noticed, and the platform measured it.

The two-file split is not incidental. Compiled code alone cannot support a profiler that needs to know which method is running, a debugger that needs to step through source, or a deoptimisation that needs to fall back to the interpreter. Keeping the bytecode alongside the native code is what makes those features possible, at the cost of storage.

The Android 7.0 correction: hybrid#

Android 7.0 (API 24) replaced full AOT with a hybrid model:

  • Install verifies the DEX and does little or no compilation.
  • At runtime, code is interpreted at first.
  • Hot methods are JIT-compiled and the JIT records a profile.
  • When the device is idle, dex2oat reads the profile and AOT-compiles the hot methods.

The profile is the hinge. It is a record of which classes and methods the app actually executed, written to a file only the app can read. Compilation guided by it is much cheaper than compiling everything and much better than compiling nothing.4

Loading diagram…
Text description of this diagram

A state machine over the ways a single method can be executed. It starts interpreted. Becoming hot moves it to JIT-compiled, with the code living in the process's code cache. Being listed in a profile moves it to AOT-compiled, with code in an oat file. A method can be both, and when both exist the JITed code is preferred at runtime. Deoptimisation moves it back to interpreted; a profile reset with the verify filter removes the compiled code entirely. Clicking a state shows the transitions available from it.

Click through the states to see how a method moves. The two loops — JIT and AOT — are separate, and a method can sit in both at once. The JIT and the AOT compiler share code and optimisations but produce different output: the JIT has runtime type information the AOT compiler lacks, and the AOT compiler can spend more time. When both exist for a method, the JITed code is preferred.4

The threshold that makes a method "hot" is a tunable property, dalvik.vm.jitthreshold, and its documented default is 10,000.5 That number is a policy, not a law of nature: lower it and more code gets compiled sooner, at the cost of more JIT work during execution.

Baseline Profiles#

The hybrid model has an obvious gap: the first run has no profile, so first launch is interpreted. A Baseline Profile fills it.

A Baseline Profile is generated by running the app's critical user journeys under Macrobenchmark and recording the classes and methods involved. It is shipped in the APK, and ART uses it to compile those methods at install. Google's documentation claims about 30% faster code execution from first launch for the covered paths.6

The mechanism matters more than the number: the profile turns "what is probably hot" from a guess into a measurement taken on a developer's machine, and it moves the compilation cost from the user's first run to the install, where it is already expected.

Boot image profiles#

App profiles cover app code. Boot image profiles cover the system: the boot classpath, the system server, and the preloaded classes. They have been supported since Android 11 and matter because their effect reaches all non-native code on the device — the AOSP documentation describes impact in double-digit percentages for performance and memory.7

The three inputs are worth knowing by name, because they are the levers a platform team tunes:

  • Boot classpath profile — which boot-classpath methods get compiled.
  • Preloaded classes — which classes Zygote loads before any app exists.
  • System server profile — which system-server methods get compiled.

Every app inherits the result through the fork, which is why a change here is felt everywhere.

Compiler filters#

dex2oat takes a filter that says how much work to do:

FilterWhat it does
verifyVerification only, no compilation
speedCompile all methods
speed-profileCompile the methods in the profile, plus class-load optimisation

The defaults are speed-profile for the boot classpath and for most apps, speed for a small set of performance-critical system apps, and verify for apps without a profile.5 That table is the whole policy in three rows: compile what you know is hot, verify what you do not.

ART as a Mainline module#

Since Android 10, ART has been part of the Mainline programme, and it ships as an APEX — a package format that can be updated without a full OS update. On Android 11 ART and libcore were packaged as an APEX; the com.android.art module is the unit that ships.2

The consequence is that runtime improvements reach devices that will never get a new Android version. A new JIT heuristic, a better GC, a compiler fix — all can arrive through a Play system update. It also means the runtime and the platform must maintain a stable interface, because they can now be different versions.

What ART optimises#

ART's compiler is not just a translator. The documented improvements include:

  • Class hierarchy analysis to devirtualise calls.
  • Inline caches in .oat files, recording runtime profile data for later AOT.
  • Loop optimisations — bounds-check elimination, unrolling, vectorisation.
  • Dexlayout, which reorders DEX sections using runtime profiles to improve locality.
8

The pattern is that ART keeps finding more places to use runtime information. The AOT compiler gets better because the profile gets better, and the profile gets better because the JIT records more.

How it evolved#

Versions10 releases
APIVersionReleasedWhat changed
194.4 KitKat2013-10-31—
215.0 Lollipop2014-11-03—
247.0 Nougat2016-08-22—
268.0 Oreo2017-08-21—
289 Pie2018-08-06—
29102019-09-03—
30112020-09-08—
31122021-10-04—
3313 Tiramisu2022-08-15—
3717 Cinnamon Bun2026-06-16—

Read that table as a pendulum. 5.0: compile everything. 7.0: compile what the profile says. 9.0: ship a profile with the app. 11: profile the system too. 12 onward: ship the runtime itself as an update. Each step moved compilation later or made the input better.

Under the hood#

Curiosity corner#

  • The JIT and the AOT compiler are the same compiler. They differ in how much time they have and what information is available.
  • A profile can be wrong. It records what users did, not what they will do; an app whose usage changes needs a new profile.
  • ART can be updated without an OS update. That was not true of Dalvik, and it changes how quickly a runtime fix can reach devices.

Try it#

See the compiler filter an app was installed with
adb shell dumpsys package com.example.app | grep -A 4 "Dexopt state"

The section name and fields differ between Android releases.

Force a profile-guided recompile
adb shell cmd package compile -m speed-profile -f com.example.app

Succeeds silently on most builds.

Check yourself#

Check yourself

1 / 12

How is a new app process created from Zygote?

0 answered · 0 correct

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.

  1. docsAndroid 4.4 APIsdeveloper.android.comaccessed 2026-10-27↩
  2. docsART mainline modulesource.android.comaccessed 2026-10-27↩
  3. docsAndroid Runtime (ART) and Dalviksource.android.comaccessed 2026-10-27↩
  4. docsJIT compilersource.android.comaccessed 2026-10-27↩
  5. docsConfigure ARTsource.android.comaccessed 2026-10-27↩
  6. docsBaseline Profiles overviewdeveloper.android.comaccessed 2026-10-27↩
  7. docsBoot image profilessource.android.comaccessed 2026-10-27↩
  8. docsART improvementssource.android.comaccessed 2026-10-27↩