AOT cache
Generate a Project Leyden AOT cache at build time and bundle it with your installer to skip JVM warmup at launch.
Nucleus can generate a Project Leyden AOT cache while it builds your app and bundle that cache into every installer. At launch the JVM replays pre-recorded class loading and JIT profiles instead of warming up from scratch, so the app reaches a responsive UI sooner. HotSpot C2 stays the JIT — the cache removes the warmup, not the compiler.
This page covers enabling the cache, writing a training run, and the platform constraints that apply.
Add the dependency
nucleus.aot-runtime is pulled in transitively by nucleus.nucleus-application. Add it explicitly only if you don't use the umbrella artifact:
dependencies {
implementation("dev.nucleusframework:nucleus.aot-runtime:2.0.7")
}Enable the cache
Set one flag in the nativeDistributions block:
nucleus.application {
mainClass = "com.example.MainKt"
nativeDistributions {
enableAotCache = true
}
}JDK 25+ required
AOT cache generation requires JDK 25 or later. Older toolchains fail the build.
That is the entire build-side setup. When the flag is on, the plugin:
- Runs your app in training mode after
createDistributable. - Records class loading and JIT compilation into
app.aotwith-XX:AOTCacheOutput. - Injects
-XX:AOTCache=$APPDIR/app.aotinto the launcher configuration. - Bundles the cache with every installer (
.dmg,.msi,.deb, and so on).
Write a training run
During training the JVM watches a real run of your app and writes the cache when the process exits. Your code must exit cleanly, or the build waits until the 300 s safety timeout force-kills it. With nucleusApplication { … }, arm the self-exit timer in one line:
import dev.nucleusframework.application.aotTraining
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.window.DecoratedWindow
import kotlin.time.Duration.Companion.seconds
fun main(args: Array<String>) = nucleusApplication(args) {
// No-op outside training. Calls exitApplication() after 45 seconds.
aotTraining(duration = 45.seconds)
if (isAotTraining) {
// The more real screens the training run touches, the more methods
// get profiled. Walk the navigation graph and preload assets.
preloadNavigationScreens()
preloadFontsAndImages()
}
DecoratedWindow(onCloseRequest = ::exitApplication, title = "MyApp") {
App()
}
}If you call Compose's application { … } directly, read the mode and drive the timer yourself:
import androidx.compose.ui.window.application
import dev.nucleusframework.aot.runtime.AotRuntime
import dev.nucleusframework.aot.runtime.AotRuntimeMode
fun main() {
if (AotRuntime.mode() == AotRuntimeMode.TRAINING) {
Thread({ Thread.sleep(45_000); System.exit(0) }, "aot-timer").start()
preloadNavigationScreens()
preloadFontsAndImages()
}
application { /* ... */ }
}How it works
The plugin sets the nucleus.aot.mode system property to training during cache generation and to runtime when the cache is loaded. AotRuntime.mode() reads that property and returns AotRuntimeMode.TRAINING, AotRuntimeMode.RUNTIME, or AotRuntimeMode.OFF when the property is unset.
A training run must:
- Exit with code 0. The
aotTrainingDSL handles this by callingexitApplication()once the timer fires. - Finish before the 300 s safety timeout.
- Be representative of a real session. The more real screens it touches, the more methods get profiled, and the faster the post-cache cold start.
On headless Linux the plugin starts Xvfb automatically when DISPLAY is unset.
Pin the platform and JDK
The AOT cache is platform- and JDK-specific. A cache built on macOS ARM64 with JBR 25 does not work on Linux x64 or with a different JDK build.
- Build on each target platform. A CI matrix is the cleanest setup.
- The plugin trains with the bundled JDK from
createDistributable, so the runtime JDK matches the training JDK byte-for-byte. setup-nucleuspins the same JBR version across OS runners. See CI/CD.
API reference
Gradle DSL
| Property | Default | Notes |
|---|---|---|
nativeDistributions.enableAotCache | false | Single switch. Triggers training and bundling. |
AotRuntime
dev.nucleusframework.aot.runtime.AotRuntime resolves the mode from the nucleus.aot.mode system property.
| API | Returns |
|---|---|
AotRuntime.mode() | AotRuntimeMode.OFF, TRAINING, or RUNTIME |
AotRuntime.isTraining() | true during a training pass |
AotRuntime.isRuntime() | true when the cache is loaded |
nucleusApplication scope
Available inside a nucleusApplication { … } block.
| Member | Equivalent |
|---|---|
aotMode | AotRuntime.mode() |
isAotTraining | aotMode == AotRuntimeMode.TRAINING |
isAotRuntime | aotMode == AotRuntimeMode.RUNTIME |
aotTraining(duration, onTimeout) | Self-exit timer for training runs (default duration 15 s, onTimeout calls exitApplication()) |
Measuring cold start
Measure from the OS-level process spawn (time ./MyApp on Linux and macOS, Measure-Command on Windows) to the first visible frame. Compare a build with enableAotCache = false against one with it on, using the same JDK and a representative training run.
Notes
- The cache is per binary — every release ships a fresh one. Don't share caches across versions.
- If you need a smaller resident set or a shorter cold start, use the GraalVM native image path instead. The AOT cache keeps HotSpot's throughput; GraalVM trades some throughput for startup time and size.
What's next
- Project setup — the umbrella artifact and module layout.
- GraalVM native image — the alternative path for the smallest, fastest cold start.
- CI/CD — build a per-platform cache in a CI matrix.
Tasks & CI
The Gradle tasks the Nucleus plugin adds for GraalVM native image, where their output lands, and how to run them on each OS in CI.
Native access — reflection metadata, resolved for you
Nucleus generates the reflection, resource, and JNI metadata a GraalVM Native Image build needs, merging it from several sources so the build compiles without hand-written JSON.