Startup and the AOT Cache

Learn where a Java service spends its start-up time and use class data sharing and the Java 25 AOT cache to start faster without changing code.

Syntaxjava -XX:AOTCacheOutput=app.aot -jar app.jar # training run (Java 25) java -XX:AOTCache=app.aot -jar app.jar # use the cache

Start-up time matters when a service scales out under load, in serverless functions, in CI pipelines that start the application for tests, and for command-line tools. A typical framework-based service spends its start-up time loading and linking thousands of classes, running static initialisers, scanning for components and then warming up in the JIT.

Class data sharing

Class data sharing (CDS) stores parsed classes in an archive that the JVM maps straight into memory. The JDK ships a default archive for its own classes and uses it automatically. Application CDS extends this to your classes and libraries:

java -XX:ArchiveClassesAtExit=app.jsa -jar app.jar   # training run
java -XX:SharedArchiveFile=app.jsa -jar app.jar       # later runs

The AOT cache

Java 24 introduced the ahead-of-time (AOT) cache, from Project Leyden. It goes further than CDS: it stores classes already loaded and linked, as they were during a training run. Java 25 made it easier to create, with a single option, and also stores method profiles in the cache so the JIT can optimise hot methods sooner.

# 1. Training run: exercise the app, then exit normally
java -XX:AOTCacheOutput=app.aot -jar app.jar

# 2. Production runs
java -XX:AOTCache=app.aot -jar app.jar

The longer Java 24 form still works: -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf for the training run, then -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot to build the cache from that configuration.

Rules that trip people up

  • The cache must be used with the same JDK build and the same class path as the training run. Build it in the Dockerfile with the exact runtime image you ship.
  • Classes must come from JAR files. A class path containing a directory of classes is rejected with an error when the cache is created.
  • If the cache cannot be used, the JVM by default starts without it, only slower, with at most a warning in the log. Add -XX:AOTMode=on to make start-up fail instead, so a stale cache cannot go unnoticed.
  • A training run that only starts and stops records start-up classes; one that also serves a few representative requests captures more.

Your own code

  • Classes are loaded and initialised on first use. Expensive static initialisers run in whichever request touches the class first, unless you trigger them at start-up on purpose.
  • Do slow work once: compile regular expressions into static final Pattern fields, build lookup tables at start-up.
  • Defer what is rarely needed, such as a report engine, with the holder idiom.
  • For the fastest start, GraalVM Native Image compiles the whole application ahead of time to a native executable, at the cost of longer builds and limits on reflection.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Add an AOT cache training run to the Docker build of a service so new pods are ready faster when the autoscaler adds them.
  • Cut the start-up time of an integration test suite that boots the application many times by sharing an application CDS archive.
  • Move regex compilation and lookup-table building out of request handlers into static final fields initialised once.

More examples

Deferring expensive set-up with the holder idiom

The engine is built on the first call to instance(), not at start-up, and the JVM guarantees class initialisation runs once even with many threads. Use this for heavy objects that most runs never need.

Example · java
static final class ReportEngine {
    private ReportEngine() {
        IO.println("report engine built");
    }

    private static class Holder {
        static final ReportEngine INSTANCE = new ReportEngine();
    }

    static ReportEngine instance() {
        return Holder.INSTANCE;
    }

    String monthly(String month) {
        return "report for " + month;
    }
}

void main() {
    IO.println("server ready");
    IO.println(ReportEngine.instance().monthly("2026-01"));
    IO.println(ReportEngine.instance().monthly("2026-02"));
}
// Output:
// server ready
// report engine built
// report for 2026-01
// report for 2026-02

Compile a pattern once

String.matches compiles the pattern each time it is called, which adds up in a per-request validator. A static final Pattern is compiled once when the class initialises and is safe to share between threads.

Example · java
import java.util.regex.Pattern;

static final Pattern ORDER_ID = Pattern.compile("ORD-\\d{4}-[A-Z]{3}");

static boolean slowCheck(String id) {
    return id.matches("ORD-\\d{4}-[A-Z]{3}"); // compiles the regex on every call
}

static boolean fastCheck(String id) {
    return ORDER_ID.matcher(id).matches();
}

void main() {
    String[] ids = {"ORD-2026-ABC", "ord-2026-abc", "ORD-26-ABC"};
    for (String id : ids) {
        IO.println(id + " " + slowCheck(id) + " " + fastCheck(id));
    }
}
// Output:
// ORD-2026-ABC true true
// ord-2026-abc false false
// ORD-26-ABC false false

Watching classes load on first use

Creating one HTTP client pulls in a large number of JDK classes the first time, and a framework does this thousands of times during start-up. That loading and linking work is exactly what CDS and the AOT cache save on later runs.

Example · java
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.net.http.HttpClient;

void main() {
    ClassLoadingMXBean classes = ManagementFactory.getClassLoadingMXBean();
    long before = classes.getTotalLoadedClassCount();

    HttpClient client = HttpClient.newHttpClient();

    long loaded = classes.getTotalLoadedClassCount() - before;
    IO.println("first HttpClient loaded more than 100 classes: " + (loaded > 100));
    IO.println("client version: " + client.version());
}
// Output:
// first HttpClient loaded more than 100 classes: true
// client version: HTTP_2

Discussion

  • Be the first to comment on this lesson.