Java Module 16 – JVM internals, memory, GC and performance
Startup: AOT cache and native images
Speed up Java start-up with the JDK 25 AOT cache: train it, create it, measure five runs each, and see where GraalVM native images fit instead.
What you will learn
- Create and use an AOT cache to improve start-up
- Measure start-up before and after with the same workload
- Compare the AOT cache with native images at a high level
Before you start
What you’ll learn · Before you start
What you will learn
- Create and use an AOT cache to improve start-up
- Measure start-up before and after with the same workload
- Compare the AOT cache with native images at a high level
Before you start
On this page
Start-up time is the time from typing java to the first useful result. Before a Java program does its own work,
the JVM loads, links and initialises every class it needs, often more than a thousand of them, and runs the program’s
code in the interpreter until the JIT compiler has compiled the busy parts. The AOT cache (ahead-of-time cache)
removes much of that work: a training run of the program records which classes it uses, and later runs find those
classes already loaded and linked in one file. JDK 24 added it (JEP 483), JDK 25 made it a single option,
-XX:AOTCacheOutput (JEP 514), and also stores method profiles in it (JEP 515). Native images, built with
GraalVM’s native-image tool, go further: they compile the whole program ahead of time into an executable for one
operating system and processor. This lesson measures the first and explains when the second is worth its costs.
Creating and using an AOT cache on JDK 25
Text description of the diagram
The diagram runs from top to bottom.
- app.jar: the application, packaged as JAR files only.
- The training run, java -XX:AOTCacheOutput=app.aot -jar app.jar, runs the real workload and notes every class that is loaded and linked, and the method profiles.
- Assembly: when the training run ends, a second JVM reads those notes and writes the cache.
- app.aot: the cache, with the classes already loaded and linked, plus the method profiles.
- Production runs, java -XX:AOTCache=app.aot -jar app.jar, map the cache into memory at start-up.
A dashed box beside the production runs states the rule: the same JDK release, operating system, processor and class path, or the JVM ignores the cache with a warning.
Where start-up time goes
A small command-line tool shows the work. It reads five invoices from XML, checks each tax registration number against its pattern, totals the amounts by state and prints a summary:
import java.io.ByteArrayInputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/** A command-line tool that reads invoices from XML, checks their GSTIN, totals them by state and prints a summary. */
public class InvoiceTool {
record Invoice(String gstin, String state, LocalDate date, BigDecimal amount) {}
static final String XML = """
<invoices>
<invoice gstin="27AAPFU0939F1ZV" state="Maharashtra" date="2026-09-01" amount="11800.00"/>
<invoice gstin="29AAGCB7383J1Z4" state="Karnataka" date="2026-09-02" amount="2360.50"/>
<invoice gstin="27AAPFU0939F1ZV" state="Maharashtra" date="2026-09-05" amount="590.00"/>
<invoice gstin="33AABCT1332L1ZZ" state="Tamil Nadu" date="2026-09-07" amount="47200.00"/>
<invoice gstin="BADGSTIN" state="Kerala" date="2026-09-09" amount="100.00"/>
</invoices>
""";
static final Pattern GSTIN = Pattern.compile("\\d{2}[A-Z]{5}\\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]");
public static void main(String[] args) throws Exception {
Logger.getLogger("invoice").fine("starting");
var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new ByteArrayInputStream(XML.getBytes(StandardCharsets.UTF_8)));
NodeList nodes = doc.getElementsByTagName("invoice");
List<Invoice> invoices = new ArrayList<>();
for (int i = 0; i < nodes.getLength(); i++) {
Element e = (Element) nodes.item(i);
invoices.add(new Invoice(e.getAttribute("gstin"), e.getAttribute("state"),
LocalDate.parse(e.getAttribute("date")), new BigDecimal(e.getAttribute("amount"))));
}
var valid = invoices.stream().filter(inv -> GSTIN.matcher(inv.gstin()).matches()).toList();
Map<String, BigDecimal> byState = valid.stream().collect(Collectors.groupingBy(Invoice::state, TreeMap::new,
Collectors.reducing(BigDecimal.ZERO, Invoice::amount, BigDecimal::add)));
var rupees = NumberFormat.getCurrencyInstance(Locale.of("en", "IN"));
long days = ChronoUnit.DAYS.between(valid.getFirst().date(), valid.getLast().date()) + 1;
System.out.printf("%d invoices, %d with a valid GSTIN, issued over %d days%n", invoices.size(), valid.size(), days);
byState.forEach((state, total) -> System.out.println(" " + state + ": " + rupees.format(total.setScale(2, RoundingMode.HALF_UP))));
}
} Indian invoices
The invoices are Indian: each registration number is a GSTIN, a business’s registration number for GST, and the
totals are formatted for the en-IN locale, with the ₹ sign and Indian digit grouping, as the tool’s output shows.
To print four lines, it uses XML parsing, regular expressions, streams, records, decimal arithmetic and currency
formatting, and each of these brings its classes, well over a thousand in all. The JDK already ships a class data
sharing (CDS) archive: about 1,400 of its own most common classes, listed in its lib/classlist file and stored in
their parsed form in a 16 MB file, lib/server/classes.jsa, which the JVM maps into memory at start-up instead of
reading those classes one by one. The tool’s other classes, from the XML parser to its own
InvoiceTool, are still read, parsed, verified and linked in every run, and the code that initialises them runs every
time too.
Creating and using an AOT cache
Since JDK 25, one extra option on a normal run creates the cache. The cache is tied to the class path, so the tool is packaged as a JAR first:
from pathlib import Path
from session import run
from startup import show, times
# The tool as a JAR, then a training run that writes an AOT cache (JDK 25, JEP 514), then five timed runs of the
# tool without the cache and five with it.
run("""
javac -d out InvoiceTool.java
jar --create --file app.jar --main-class InvoiceTool -C out .
java -jar app.jar
java -XX:AOTCacheOutput=app.aot -jar app.jar > /dev/null
""")
print(f"# The cache: {Path('app.aot').stat().st_size / 1e6:.1f} MB")
print("# Five runs each, wall-clock time from start to exit")
show("java -jar app.jar", times("java -jar app.jar"))
show("... -XX:AOTCache=app.aot", times("java -XX:AOTCache=app.aot -jar app.jar")) Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac -d out InvoiceTool.java $ jar --create --file app.jar --main-class InvoiceTool -C out . $ java -jar app.jar 5 invoices, 4 with a valid GSTIN, issued over 7 days Karnataka: ₹2,360.50 Maharashtra: ₹12,390.00 Tamil Nadu: ₹47,200.00 $ java -XX:AOTCacheOutput=app.aot -jar app.jar > /dev/null Picked up JAVA_TOOL_OPTIONS: -Djava.class.path=app.jar -XX:AOTCacheOutput=app.aot -XX:AOTConfiguration=app.aot.config -XX:AOTMode=create # The cache: 17.0 MB # Five runs each, wall-clock time from start to exit java -jar app.jar 768 813 750 576 766 median 766 ms ... -XX:AOTCache=app.aot 714 424 246 212 373 median 373 ms
This output changes from run to run: Times change from run to run, and more on a busy machine; the size of the cache can change slightly.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 startup_session.py
-XX:AOTCacheOutput=app.aot turns the run into a training run. The tool runs as usual, and the JVM notes every
class it loads and links. When the tool ends, the JVM starts a second JVM that reads those notes and writes the cache:
the line Picked up JAVA_TOOL_OPTIONS comes from that second JVM, which received its instructions through an
environment variable. Later runs use the cache with -XX:AOTCache=app.aot.
In the recorded runs, the cache was 17 MB, and the median start-up fell from 766 ms to 373 ms, about half. Each
figure is the median of five runs, measured from starting the process to its exit, on a computer that was busy with
other work, which is why single runs vary by a few hundred milliseconds. The cache is also specific: JEP 483 lists
the same JDK release, the same operating system and processor, and the same class path, made of JAR files only, as
conditions for using it. With -XX:AOTMode=on, a JVM that cannot use its cache stops with an error instead of starting without
it.
The training run should do what production does, because the cache can only hold what that run loaded and, since JDK 25, how its methods behaved; JEP 515 states plainly that the benefit depends on the training run resembling production. A training run that only prints a help text produces a small cache that covers little of a real start-up. The gain also grows with the number of classes a program loads beyond the JDK’s own archive: JEP 483 reports Spring PetClinic, which loads about 21,000 classes, starting in 2.604 seconds instead of 4.486 with a 130 MB cache.
Three ways to start faster, compared
| Criterion | CDS archive of the JDK | AOT cache (JDK 25) | Native image (GraalVM) |
|---|---|---|---|
| What it holds | The JDK's own common classes, parsed | Your application's classes, loaded and linked, plus method profiles | The whole program compiled to machine code |
| Extra build step | None: it ships with the JDK | One training run | A native-image build of the application |
| What still runs at start-up | Loading of every other class | Initialisation and the JIT warm-up | Little: no class loading or JIT warm-up |
| Main restriction | None | The same JDK, platform and class path in every run | Dynamic features need metadata at build time |
| Output runs on | Any platform of that JDK | The platform it was trained on | One operating system and processor |
The JDK’s archive is always there. An AOT cache asks one extra run and keeps everything else about the JVM, including its JIT compiler. A native image changes the deployment: GraalVM’s documentation describes it as starting in milliseconds without warm-up, and as needing configuration for reflection, dynamic proxies and resources.
Check yourself
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Measuring start-up honestly: classes, a tiny program and warm-up Pro: opens with a Pro, Premium or Ultimate pass
A start-up figure means something only with what was measured and how. This part counts where the classes come from, shows a program for which a cache changes nothing, and measures the method profiles that JDK 25 adds to the cache.
In this part:
- Where each class came from, with and without the cache
- A program too small to gain
- Method profiles and the first seconds of work
5 runnable examples (Java, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Stale caches, wrong training runs, and what they cost Pro: opens with a Pro, Premium or Ultimate pass
An AOT cache is an optimisation, so a JVM that cannot use it starts anyway. This part rebuilds the JAR to make the cache stale, measures what that costs, makes the failure fatal on purpose, and shows a newer JDK refusing the cache. Then it is your turn, with a cache that the JVM accepts but that was trained on the wrong command.
In this part:
- A rebuilt JAR, a stale cache, and a strict mode
- Building the cache where it cannot go stale
- Your turn: a cache trained on the wrong command
3 runnable examples (Java, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
AOT cache or native image: a decision with its costs Pro: opens with a Pro, Premium or Ultimate pass
The two techniques answer the same complaint, slow start-up, with very different bills. This part sets out what each one costs, where each fits, and what the next JDK releases change.
In this part:
- What a native image changes, and what it costs
- Choosing for four kinds of programs, with the trade-offs
- What the next releases add
about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is the AOT cache in JDK 25, and how do you create and use one?
The AOT (ahead-of-time) cache is a file that holds an application's classes already loaded and linked, plus, since JDK 25, method profiles, so that later runs skip that work at start-up. You create it with a training run of the application: java -XX:AOTCacheOutput=app.aot -jar app.jar runs the program as usual, and when it ends the JVM writes the cache (JEP 514). Production runs use it with java -XX:AOTCache=app.aot -jar app.jar. The cache only works with the same JDK release, operating system, processor and class path, made of JAR files, as the training run; otherwise the JVM warns and starts without it. The gain depends on the program: large applications that load many classes gain most, and a tiny program may gain nothing measurable.
-
Mid to senior
Your team added an AOT cache to a service's container image, but production start-up did not improve at all. What would you check, and in what order?
-
Senior to staff
A Java serverless function takes 1.8 s to start and 200 ms to handle a request. Compare an AOT cache and a GraalVM native image for it, and say what you would measure before deciding.
-
Mid to senior
After a change to the build pipeline, a command-line tool shipped with an AOT cache starts more slowly than the same tool with no cache at all. Explain how that can happen, and how you would make the pipeline safe against it.
Key takeaways
- Start-up is class loading, linking, initialisation and interpreted code before the JIT compiler catches up.
- On JDK 25,
java -XX:AOTCacheOutput=app.aottrains and writes an AOT cache, and-XX:AOTCache=app.aotuses it. - Measure start-up as a median of several runs from process start to exit, with the same workload before and after.
- A native image starts faster still, but it is a different build with different rules.
References
- JEP 483: Ahead-of-Time Class Loading & Linking (OpenJDK)
- JEP 514: Ahead-of-Time Command-Line Ergonomics (OpenJDK)
- JEP 515: Ahead-of-Time Method Profiling (OpenJDK)
- JEP 544: Ahead-of-Time Code Compilation (OpenJDK)
- The java Command (JDK 25 tool reference), Ahead-of-Time Cache (Oracle)
- Class Data Sharing (JDK 25 HotSpot Virtual Machine Guide) (Oracle)
- Native Image (GraalVM reference manual) (Oracle (GraalVM))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress