AOT-Caching ist 2026 kein Experiment mehr. Mit JDK 24 bis 26 gibt es einen belastbaren Pfad, um Java-Anwendungen schneller in einen produktiven Zustand zu bringen, ohne dabei die Kompatibilität und Dynamik der JVM aufzugeben. Project Leyden verlagert wiederholbare JVM-Arbeit in einen kontrollierten Trainings- und Build-Kontext und stellt sie als Cache zur Laufzeit bereit. Java hat in den letzten Jahrzehnten vieles richtig gemacht: Es steht für eine robuste Laufzeit, ein reifes Ökosystem und eine bemerkenswerte Kompatibilität über Plattformen und Sprachversionen hinweg. Dabei fiel eine Schwäche lange kaum ins Gewicht: der vergleichsweise aufwendige Startpfad. In klassischen Betriebsmodellen liefen Anwendungen stabil über Wochen oder Monate durch. Ob der Start zwanzig oder sechzig Sekunden dauerte, war oft zweitrangig.
Stay tuned
Regelmäßig News zur Konferenz und der JAX-Community erhalten
In cloud-nativen Architekturen ist diese Sichtweise nicht mehr tragfähig. Instanzen werden häufiger neu gestartet, horizontal skaliert, geupdatet und gezielt ersetzt. Bei Lastspitzen ist entscheidend, wie schnell eine neue Instanz wirklich nutzbar wird. Im Fehlerfall bestimmt der Wiederanlauf über die Verfügbarkeit, und bei kurzen Lebenszyklen schmälert jede unproduktive Startphase direkt das Infrastrukturbudget. Somit ist Start-up-Zeit nicht mehr nur eine technische, sondern auch eine betriebswirtschaftliche und ökologische Größe.
Genau diesen Punkt adressiert Project Leyden [1]. Leitmotiv ist, Berechnungen zeitlich nach vorn zu verlagern: Was sonst bei jedem Start erneut stattfinden würde, wird einmalig in einem Trainings- und Build-Kontext beobachtet, als Artefakt gesichert und zur Laufzeit wiederverwendet. Der Ansatz ersetzt die JVM nicht und führt auch nicht zu einem Closed-World-Modell wie GraalVM Native Image. Das dynamische Verhalten der JVM bleibt erhalten, während Start- und Warm-up-Pfade gezielt entlastet werden.
AOT-Caching eignet sich somit besonders für Teams, die schnelle, reproduzierbare Verbesserungen auf der Standard-JVM erzielen möchten, ohne die Plattform zu wechseln. Der vorliegende Artikel erläutert die technischen Grundlagen sowie die wichtigsten Workflows für Java und Spring Boot und stellt die Betriebsregeln vor, die einen Cache von einem netten Experiment in ein belastbares Produktionsartefakt verwandeln.
Start-up ist heute ein Betriebsproblem
Um den Nutzen von AOT-Caching realistisch bewerten zu können, ist es zunächst wichtig, eine saubere Messung durchzuführen. In der Praxis hat sich die Unterscheidung zweier Messpunkte bewährt. Erstens der Zeitpunkt tready, ab dem eine Instanz die erste sinnvolle Anfrage stabil bedient. Zweitens der Zeitpunkt tsteady, ab dem Durchsatz und Latenz unter Last in einem stabilen Bereich liegen. Diese Unterscheidung ist essenziell, denn ein erfolgreicher Health-Check sagt noch nichts über den Lastzustand aus. Viele Anwendungen melden sich frühzeitig bereit, obwohl sie intern noch mitten im Warm-up stecken.
Das ökonomische Argument folgt unmittelbar. Eine Instanz, die bereits Ressourcen belegt, aber noch nicht produktiv arbeitet, erzeugt Leerkosten. Gleichzeitig steigt der Energiebedarf während Start-up und Warm-up, ohne dass in dieser Zeit bereits ein proportionaler Nutzwert entsteht. Bei vielen Tausend Container-Starts pro Tag summiert sich das spürbar – sowohl auf der Cloud-Rechnung als auch im CO2-Profil der Plattform. Abbildung 1 fasst die typische Sequenz vom kalten Start bis zur stabilen Lastphase zusammen und macht die beiden Messpunkte sichtbar.

Abb. 1: Vom kalten Start bis zur stabilen Lastphase
Für Teams ergibt sich daraus die Pflicht, jede Optimierungsvariante mit demselben Mess-Set zu bewerten. Dabei sollten mindestens tready, tsteady, das Speicherprofil und ein reproduzierbares Lastszenario dokumentiert sein. Erst dann lassen sich Exploded JAR, Class Data Sharing, AOT-Cache und gegebenenfalls Native Image sinnvoll vergleichen. Ohne diese Baseline bleibt jeder Speed-up eine gefühlte Wahrheit.
Konkrete Größenordnungen helfen bei der Einschätzung der Erwartungen. Das Spring-Team berichtet für die bekannte PetClinic-Anwendung von Start-up-Zeiten, die sich durch CDS und AOT-Cache von rund drei auf etwa eine Sekunde reduzieren – ein Faktor von zwei bis drei, der für klassische Spring-Boot-Services typisch ist. Bei Plain-Java-Anwendungen ist der relative Effekt geringer, da weniger Klassenlade- und Konfigurationsarbeit eingespart werden kann. In Microservice-Flotten mit vielen kurzlebigen Instanzen ist nicht der einzelne Lauf der eigentliche Gewinn, sondern die Summe über Tausende Container-Starts pro Tag.
Dabei ist weniger die konkrete Zahl entscheidend als vielmehr die Reproduzierbarkeit der Messmethode. Jedes Team sollte die Baseline auf den eigenen Workloads ermitteln, statt sich auf fremde Benchmarks zu verlassen.
Project Leyden ist kein einzelnes Feature, sondern ein mehrjähriges OpenJDK-Programm. Die Entwicklung lässt sich an vier JEPs gut nachvollziehen: JEP 483 in JDK 24 bringt Ahead-of-Time Class Loading and Linking [2], JEP 514 in JDK 25 verbessert die Ergonomie auf der Kommandozeile [3], JEP 515 ergänzt Ahead-of-Time Method Profiling – die Profiling-Daten heißer Methoden werden direkt im AOT-Cache gespeichert (es werden keine separate Datei, keine zusätzlichen Flags benötigt); der JIT-Compiler kann diese Daten beim Start sofort konsumieren [4] – und JEP 516 in JDK 26 schließlich hebt die GC-Einschränkung beim Object Caching auf und öffnet den Weg für ZGC [5]. Die Botschaft ist klar: AOT-Caching ist kein exotischer Sonderpfad, sondern ein graduell gereiftes JVM-Feature mit konkretem Betriebsnutzen. Tabelle 1 fasst den Funktionsumfang pro JDK-Release zusammen.
| JDK | Neue Fähigkeit | Praktischer Effekt |
|---|---|---|
| 24 | JEP 483 – AOT Class Loading & Linking | Drei-Phasen-Workflow mit AOTMode=record/create |
| 25 | JEP 514 – CLI-Ergonomie | Ein-Schritt-Training mit AOTCacheOutput |
| 25 | JEP 515 – Method Profiling | JIT erhält Profil-Daten schon beim Start |
| 26 | JEP 516 – Object Caching mit beliebigem GC | AOT-Cache auch mit ZGC nutzbar |
Tabelle 1: Leyden-Fähigkeiten pro JDK-Release
Gleichzeitig ist AOT-Caching weiterhin Teil eines Werkzeugkastens. Exploded JARs, CDS [10], AOT-Cache, CRaC und Native Image weisen unterschiedliche Stärken und Grenzen auf. Es geht nicht um Glaubensfragen, sondern um Trade-offs pro Workload. Tabelle 2 ordnet die wichtigsten Verfahren ein und macht deutlich, warum AOT-Caching für viele Teams der pragmatischste nächste Schritt ist.
| Ansatz | Stärken | Grenzen | Typischer Einsatz |
|---|---|---|---|
| Exploded JAR | sehr niedrige Einstiegshürde, schnellerer Classpath-Start | kein tieferes Runtime-Tuning | schneller Quick-Win |
| CDS | etabliert, robust, gut verstanden | begrenzter Optimierungsumfang | konservative Produktionsumgebungen |
| AOT-Cache | guter Kompromiss aus Nutzen und Kompatibilität | erfordert diszipliniertes Training | cloud-native JVM-Workloads |
| CRaC | extrem schneller Restore | Snapshot- und Betriebskomplexität, Linux/CRIU | Spezialfälle mit harten Start-up-Zielen |
| Native Image | sehr schneller Kaltstart, geringer Footprint | Closed-World, höherer Build-Aufwand | Serverless- und CLI-Szenarien |
Tabelle 2: Einordnung gängiger Start-up-Strategien
Tabelle 1 zeigt zugleich eine andere wichtige Eigenschaft: Jedes Release baut additiv auf dem vorherigen auf. Wer heute mit JDK 24 startet, hat einen klaren Upgrade-Pfad in Richtung JDK 25 und 26, ohne den Workflow grundlegend umstellen zu müssen.
Project Leyden: das Prinzip hinter dem AOT-Cache
Das eigentliche Prinzip von Leyden ist einfach formuliert, seine Wirkung jedoch weitreichend. Statt jeden Programmstart als isoliertes Ereignis zu behandeln, wird die JVM einmalig in einem repräsentativen Lauf beobachtet. Welche Klassen werden geladen, welche werden gelinkt, welche Methoden sind heiß? Die Antworten auf diese Fragen wandern in eine Konfiguration und anschließend in einen Cache. Spätere Starts können diesen Cache laden und überspringen damit große Teile der Arbeit, die sonst bei jedem Hochfahren neu anfallen würde.
Diese Mechanik ähnelt nicht zufällig der JIT-Welt. Auch dort beobachtet die Laufzeit reales Verhalten und nutzt diese Informationen, um später schneller zu sein. Der Unterschied liegt in der zeitlichen Verlagerung: Während JIT zur Laufzeit lernt, erfolgt das Lernen beim AOT-Cache bereits zur Build-Zeit – allerdings auf Basis echter Beobachtungen und nicht ausschließlich durch statische Analyse. Genau diese Kombination unterscheidet Leyden von Ansätzen wie GraalVM Native Image, die auf einer geschlossenen Weltannahme aufbauen und daher auf einen Teil der Java-Dynamik verzichten müssen.
Aus diesem Ergebnis lässt sich die folgende Linie für Architekturentscheidungen ableiten. Wer schnellen Start-up ohne Verlassen der JVM wünscht, ist beim AOT-Cache richtig. Wer dagegen einen Millisekunden-Kaltstart und einen minimalen Footprint benötigt und die Closed-World-Einschränkungen akzeptiert, hat mit Native Image weiterhin die passende Lösung. Beide Ansätze sind komplementär und schließen einander nicht aus. Der AOT-Cache ist häufig der erste Schritt, weil er ohne Migration auskommt.
Wie AOT-Caching technisch funktioniert
Drei-Phasen-Workflow als robuste Basis
Der fachliche Kern besteht aus drei Schritten: Zunächst werden Beobachtungen aus einem Trainingslauf als Konfiguration festgehalten. Daraus wird ein Cache erzeugt, der beim späteren Start geladen wird. Praktisch wichtig ist die Trennung dieser Schritte, da sie die Reproduzierbarkeit und Fehlersuche erheblich vereinfacht. Mit JDK 24 war der explizite Drei-Phasen-Workflow, wie ihn Listing 1 zeigt, der Normalfall. Er ist auch heute noch relevant, wenn Training und Assembly in unterschiedlichen Build-Umgebungen stattfinden oder wenn Teams den Prozess granular steuern müssen.
Listing 1: Drei-Phasen-Workflow (Record, Create, Runtime)
# Phase 1: Training/Recording
java -XX:AOTMode=record \
-XX:AOTConfiguration=build/app.aotconf \
-cp build/app.jar com.example.App
# Phase 2: Assembly/Cache creation
java -XX:AOTMode=create \
-XX:AOTConfiguration=build/app.aotconf \
-XX:AOTCache=build/app.aot \
-cp build/app.jar
# Phase 3: Deployment/Runtime
java -XX:AOTCache=build/app.aot \
-cp build/app.jar com.example.App
Zwei-Phasen-Ergonomie ab JDK 25
Ab JDK 25 wird der Ablauf mit AOTCacheOutput ergonomischer, weil Training und Assembly in einem einzigen Aufruf zusammengeführt werden können [3]. Listing 2 zeigt diese Kurzform. Dadurch wird die Zahl der Kommandos reduziert und die Einstiegshürde im lokalen Entwicklungsalltag spürbar gesenkt. Die fachlichen Regeln bleiben dabei unverändert. Trainingsqualität und Konsistenz der Umgebung entscheiden weiterhin über den realen Nutzen.
Listing 2: Zwei-Phasen-Workflow (JDK 25+)
# Training und Assembly in einem Schritt
java -XX:AOTCacheOutput=build/app.aot \
-cp build/app.jar com.example.App
# Deployment
java -XX:AOTCache=build/app.aot \
-cp build/app.jar com.example.App
Der zweistufige Pfad startet intern einen Sub-Prozess mit einer ähnlichen Heap-Größe wie die ursprüngliche JVM. Das ist auf Entwicklungsrechnern unkritisch, kann in CI-Runnern mit knappen Speicherlimits jedoch zu Engpässen führen. In solchen Umgebungen lohnt der bewusste Rückgriff auf den Drei-Phasen-Weg. Die Regel lautet: Der komfortable Pfad ist nicht automatisch der robusteste für jede Pipeline. Wer reproduzierbar deployen möchte, sollte die Build-Variante wählen, die zum eigenen CI/CD-Profil passt, und sich nicht nur auf die kürzeste Kommandozeile konzentrieren.
Verifikation und striktes Laden
Für den Betrieb ist Verifikation Pflicht. Zwei Schalter helfen unmittelbar: ein strikter Modus, der einen unbrauchbaren Cache als Fehler meldet, sowie ein aussagekräftiges Logging, das das Laden von Klassen und das AOT-Verhalten sichtbar macht. Listing 3 zeigt beide Mechanismen nebeneinander. Wichtig: -XX:AOTMode=on ist laut JEP 483 ausdrücklich als Diagnose-Schalter gedacht und sollte in Produktion vermieden werden, da vom Cloud-Anbieter zugeschaltete JVMTI-Agents den Cache unter Umständen invalidieren und den Start fehlschlagen lassen können [2]. Der richtige Ort für den strikten Modus ist die CI/CD-Pipeline und das Staging. In Produktion bleibt es bei -XX:AOTCache=…, ergänzt um Logging und Metriken, die einen stillschweigend deaktivierten Cache schnell sichtbar machen.
Ein Detail zum Logging: Das früher übliche -Xlog:cds ist mit dem AOT-Cache in -Xlog:aot aufgegangen. CDS-Schalter bleiben aus Kompatibilitätsgründen funktional, neue Diagnosen sollten aber die aot-Tags verwenden.
Listing 3: Strikter Modus und log-basierte Verifikation
# Fehler, wenn der Cache nicht nutzbar ist
java -XX:AOTCache=build/app.aot \
-XX:AOTMode=on \
-cp build/app.jar com.example.App
# Sichtbarkeit für Ladepfad und AOT-Verhalten
java -XX:AOTCache=build/app.aot \
-Xlog:aot=info,class+load=info \
-cp build/app.jar com.example.App
Spring Boot: vom Konzept zur produktiven Routine
Extracted Layout statt Fat JAR
In Spring-Boot-Systemen entfaltet AOT-Caching seinen Nutzen, sobald die Prozesskette steht: Artefaktstruktur vorbereiten, Training reproduzierbar durchführen, Runtime mit Cache starten und den Effekt sauber messen. Der erste und häufigste Stolperstein ist das Packaging. Ein klassisches Fat JAR ist für den AOT-Cache-Pfad ungünstig, weil verschachtelte JAR-Strukturen sowie der interne Zugriffspfad die Cache-Anforderungen erschweren. Der produktive Weg beginnt deshalb mit dem extrahierten Layout. Spring Boot stellt dafür seit Version 3.3 einen offiziellen Tool-Modus bereit, der das Fat JAR in eine flache Struktur aus Anwendungs-JAR und einem lib-Verzeichnis zerlegt. Die hier gezeigte AOT-Cache-Workflow-Dokumentation gilt ab Spring Boot 3.5 [6], [8]. Listing 4 zeigt den passenden Aufruf.
Listing 4: Spring-Boot-Artefakt extrahieren
java -Djarmode=tools \
-jar target/aot-cache-demo.jar \
extract --destination extracted
Trainingslauf mit Exit-on-Refresh
Für den Trainingslauf hat sich in vielen Teams folgende pragmatische Strategie etabliert: Die Anwendung wird bis zum Context-Refresh gestartet und dann gezielt beendet. In Spring Boot lässt sich dieses Verhalten mit der Property spring.context.exit=onRefresh sauber steuern [6]. Der Lauf instanziiert alle nicht-lazy Singletons, ruft afterPropertiesSet auf, durchläuft den Lifecycle bis zum ContextRefreshedEvent und beendet sich anschließend kontrolliert. Damit deckt der Trainingsschritt den vollständigen Start-up-Pfad ab, ohne externe Abhängigkeiten wie eine produktive Datenbank anzusprechen. Listing 5 fasst den Aufruf zusammen.
Listing 5: Reproduzierbarer Trainingslauf für Spring Boot
cd extracted
java -XX:AOTCacheOutput=app.aot \
-Dspring.context.exit=onRefresh \
-jar aot-cache-demo.jar
Runtime und Spring AOT
Wie Listing 6 zeigt, bleibt die Runtime-Phase bewusst schlicht. Das ist ein Vorteil im Betrieb, da die Startlogik nicht mit Spezialcode angereichert wird, sondern allein über JVM-Parameter gesteuert wird. Dadurch bleibt der Anwendungscode frei von Cache-Logik und die Cache-Aktivierung ist eine reine Deploymententscheidung.
Listing 6: Start mit aktivem AOT-Cache
cd extracted
java -XX:AOTCache=app.aot \
-Xlog:aot=info \
-jar aot-cache-demo.jar
Ein häufiges Missverständnis in Projekten ist die Gleichsetzung von Spring AOT und JVM AOT-Cache. Wie Tabelle 3 gegenüberstellt, arbeiten beide auf unterschiedlichen Ebenen mit unterschiedlichen Zielen. Spring AOT ist eine Build-Time-Optimierung im Framework, bei der Reflection-Hints generiert, Bean-Definitionen vorbereitet und Konfigurationsklassen aufgelöst werden. Der JVM AOT-Cache wirkt hingegen unterhalb des Frameworks und optimiert die Arbeit innerhalb der HotSpot-Laufzeit selbst. In Kombination liefern beide Ansätze regelmäßig den besten Effekt. Wer von der JVM auf GraalVM Native Image wechseln möchte, ist auf Spring AOT angewiesen. Auf der klassischen JVM ist es ein Add-on, das selektiv aktiviert werden kann [6], [7].
| Aspekt | AOT-Cache | Spring AOT |
|---|---|---|
| Ebene | JVM / HotSpot | Spring Framework |
| Primäres Ziel | Start-up und Warm-up der JVM verbessern | Build-time-Vorbereitung von Framework-Arbeit |
| Pflicht für Native Image | nein | ja |
| Kombinierbar | ja | ja |
Tabelle 3: Unterschiedliche Ebenen, kombinierbarer Nutzen
Für Teams ergibt sich daraus eine klare Reihenfolge beim Rollout. Zuerst Baseline messen, dann AOT-Cache in die Build-Pipeline integrieren, anschließend prüfen, ob zusätzliches Spring AOT in der konkreten Anwendung den erwarteten Mehrwert liefert. Diese Schrittfolge reduziert das Risiko und macht die Wirkung jeder einzelnen Maßnahme nachvollziehbar.
Stay tuned
Regelmäßig News zur Konferenz und der JAX-Community erhalten
Trainingsläufe richtig gestalten
Die Qualität des Caches steht und fällt mit der Qualität des Trainings. Ein Cache, der nur triviale Pfade durchlaufen hat, liefert in Produktion wenig Nutzen. Umgekehrt erzeugt ein überfrachtetes Training unnötig große Artefakte und verlangsamt den Build, ohne dass die zusätzlichen Klassen und Methoden in der Realität benötigt werden. Das Ziel ist ein repräsentatives, aber fokussiertes Trainingsprofil. In Bestandsprojekten lohnt es sich, drei bis fünf kritische User-Journeys zu identifizieren und sie als Trainings-Harness zu kodifizieren.
In der Praxis haben sich zwei Strategien etabliert. Die erste ist der schlanke Lifecycle-Run mit Exit-on-Refresh. Er ist schnell, stabil in CI und liefert vor allem Klassenlade-Daten für den Start-up-Pfad. Die zweite Strategie ist ein Smoke- oder Integrationsprofil, das gezielt zentrale API-Pfade anfährt. Damit gewinnt man auch Profiling-Informationen über die heißen Methoden des Warm-up-Pfads. Das kann insbesondere ab JDK 25 mit Method Profiling den Unterschied zwischen einem moderaten und einem deutlichen Speedup ausmachen. Tabelle 4 stellt die drei praktikablen Trainingsvarianten mit ihrem jeweiligen Aufwand-Nutzen-Verhältnis gegenüber.
| Strategie | Aufwand | Codeabdeckung | Profiling-Informationen |
|---|---|---|---|
| Exit bei Context Refresh | niedrig | Fokus auf Startpfad und Klassenladung | begrenzt |
| Smoke- oder Integrationsprofil | mittel | breitere Hot Paths | gut |
| Produktionsnahe Lastsequenz | hoch | Hot Paths plus Randfälle | sehr gut |
Tabelle 4: Drei praktikable Trainingsstrategien
Um Trainingsläufe stabil und reproduzierbar zu halten, sind kontrollierte Testbedingungen entscheidend. Lokale oder containerisierte Datenquellen ersetzen produktive Systeme, durch mockbare Integrationen werden Trainings frei von externen Abhängigkeiten gehalten und kurze Lastsequenzen sorgen dafür, dass der Build-Schritt nicht zum Engpass wird. Eine produktive Datenbank, eine echte Message-Queue oder ein Authentication-Provider haben im Trainingslauf nichts zu suchen – nicht zuletzt aus Sicherheitsgründen. Dazu später mehr.
Zur Diagnose lohnt sich der Einsatz des Java Flight Recorders. Mit seiner Hilfe wird sichtbar, welche Klassen tatsächlich geladen wurden und welche Methoden während des Trainings heiß waren. Das verhindert einen Blindflug bei der Optimierung und macht regressive Veränderungen früh sichtbar, beispielsweise wenn ein eingeführtes Test-Framework plötzlich Tausende zusätzlicher Klassen in den Cache zieht. Listing 7 zeigt die drei Schritte vom JFR-Setup bis zur Auswertung.
Listing 7: Trainingslauf mit Java Flight Recorder beobachten
# 1) Klassenlade-Events aktivieren
jfr configure --output custom.jfc jdk.ClassLoad#enabled=true
# 2) Training mit Recording durchführen
java -XX:StartFlightRecording:settings=custom.jfc,duration=60s,filename=/tmp/AOT.jfr \
-cp app.jar com.example.App
# 3) Relevante Events auswerten
jfr print --events "jdk.ClassLoad" /tmp/AOT.jfr
jfr view hot-methods /tmp/AOT.jfr
Ein einfacher Smoke-Harness reicht oft aus, um realistischere Trainingspfade zu erreichen. Wichtig ist nicht die Menge der Aufrufe, sondern ihre Relevanz für den produktiven Einstiegspfad. Erfahrungsgemäß decken drei Curl-Aufrufe gegen die Health-, die meistgenutzte Lese- und die wichtigste Schreibroute bereits einen sehr großen Teil des Hot Paths ab. Listing 8 skizziert ein solches Minimal-Setup.
Listing 8: Minimaler Smoke-Harness für ein repräsentatives Training
#!/usr/bin/env bash
set -euo pipefail
curl -sf http://localhost:8080/actuator/health >/dev/null
curl -sf http://localhost:8080/api/orders >/dev/null
curl -sf http://localhost:8080/api/customers >/dev/null
In Service-Landschaften mit vielen Komponenten ist es sinnvoll, pro Service ein eigenes Trainingsprofil zu pflegen, statt ein universelles Skript zu erzwingen. So bleiben die Caches klein, reproduzierbar und fachlich nachvollziehbar. Der Wartungsaufwand pro Service ist gering, der Effekt auf Cache-Größe und Speed-up oft beträchtlich.
Anforderungen, Konsistenz und Cache-Invalidierung
AOT-Caches sind bewusst eng an ihre technische Umgebung gebunden. Das ist jedoch keine Schwäche, sondern die Voraussetzung für belastbare Ergebnisse. Wer diese Regeln ignoriert, bekommt im besten Fall keinen Speed-up, im schlechteren Fall einen stillschweigend deaktivierten Cache, der erst beim genauen Hinsehen auffällt. Tabelle 5 fasst die notwendigen Bedingungen zusammen.
| Bereich | Notwendige Bedingung |
|---|---|
| JDK | identische oder kompatibel freigegebene Version zwischen Training und Runtime |
| Plattform | gleiche Hardware-Architektur (möglichst identisches OS-Image) |
| Classpath | konsistente JAR-Struktur; Runtime darf eine Obermenge des Trainings sein |
| Moduloptionen | –add-opens, –add-exports, –add-reads, –patch-module, –upgrade-module-path dürfen nicht verwendet werden |
| Artefaktstruktur | stabile Inhalte und reproduzierbare JAR-Timestamps |
Tabelle 5: Anforderungen an einen belastbaren AOT-Cache
Aus diesen Anforderungen ergibt sich unmittelbar eine Liste der Ereignisse, bei denen ein Cache invalidiert werden muss. Jede Codeänderung, jedes Dependency-Update, jedes JDK-Upgrade, jede Veränderung der Build-Artefaktstruktur erzwingt einen Rebuild. In der Praxis ist das weniger dramatisch, als es klingt, da dieser Trainingsschritt ohnehin in die CI/CD-Pipeline gehört und pro Build automatisch ausgeführt wird. Eine Cache-Wiederverwendung über mehrere Versionen hinweg sollte explizit vermieden werden, auch wenn sie technisch möglich erscheint. Der eingesparte Build-Schritt ist den Diagnoseaufwand bei Inkonsistenzen nicht wert.
Ein in diesem Zusammenhang bislang oft unterbelichteter Aspekt ist die Sicherheit. Trainingsläufe haben in produktiven Umgebungen nichts verloren und sie dürfen keine produktiven Geheimnisse berühren. Dazu zählen Datenbank-Passwörter, API-Schlüssel, OAuth-Client-Secrets und Tokens. Trainings sollten ausschließlich gegen Dummy-Konfigurationen, lokale H2-Instanzen oder Testcontainer laufen. Die Policy lautet: Keine produktiven Secrets in Build-Time-Ausführungen! CI/CD-Pipelines sollten dies durch Secrets-Scanner und klar getrennte Berechtigungsräume aktiv durchsetzen.
Hinsichtlich der Garbage Collection gilt eine wichtige Übergangsregel. In JDK 24 und JDK 25 werden gecachte Objekte in einem GC-spezifischen Format gespeichert, das mit Serial, Parallel und G1 kompatibel ist, nicht jedoch mit ZGC. Wer also bisher ZGC einsetzt, muss sich zwischen niedrigen GC-Latenzen und AOT-Cache entscheiden. Mit JEP 516 in JDK 26 entfällt diese Einschränkung. Ein zusätzliches, GC-neutrales Streaming-Format macht AOT-Object-Caching auch mit ZGC möglich, was insbesondere für Anwendungen mit großem Heap und niedrigen Latenzanforderungen relevant ist [5].
Container und CI/CD: vom reproduzierbaren Build zum skalierbaren Deployment
Der größte operative Vorteil entsteht, wenn Cache-Erzeugung und -Nutzung in derselben kontrollierten Pipeline erfolgen. Für Teams bieten sich dabei zwei stabile Wege an: Spring Boot Buildpacks für Standardisierung oder ein mehrstufiges Dockerfile für maximale Transparenz. Beide Wege haben ihre Berechtigung und schließen einander nicht aus.
Paketo-Buildpacks für standardisierte Pipelines
Aktuelle Versionen der Paketo-Java-Buildpacks unterstützen neben CDS auch den AOT-Cache. Die Aktivierung erfolgt über Build-Properties, der Trainingslauf läuft im Build-Container automatisch. Das hat den charmanten Effekt, dass dasselbe JVM-Image für Training und Deployment verwendet wird, sodass die Konsistenzanforderungen aus Tabelle 5 damit per Konstruktion erfüllt sind. Die konkreten Variablennamen und die unterstützten JVM-Versionen ändern sich mit den Releases der Java- und JVM-Buildpacks. Die jeweils aktuellen Werte sollten direkt der Paketo-Dokumentation entnommen werden [9]. Listing 9 zeigt das Schema einer typischen Konfiguration im Spring-Boot-Maven-Plugin.
Listing 9: AOT-Cache im Maven-Buildpack-Setup
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<env>
<BP_JVM_VERSION>25</BP_JVM_VERSION>
<BP_JVM_AOTCACHE_ENABLED>true</BP_JVM_AOTCACHE_ENABLED>
<BP_SPRING_AOT_ENABLED>true</BP_SPRING_AOT_ENABLED>
</env>
</image>
</configuration>
</plugin>
Wer mehr Kontrolle wünscht, beispielsweise um den Trainingslauf gegen profilingrelevante Endpunkte auszuführen statt nur Exit-on-Refresh zu nutzen, greift weiterhin zum eigenen Dockerfile. Ein mehrstufiges Setup trennt Build, Training und Runtime sauber voneinander und macht so jede Phase einzeln reproduzierbar. Dabei ist eine wichtige Regel unübersehbar: Die Training- und Runtime-Stage müssen exakt dasselbe JVM-Basis-Image verwenden, da die JVM sonst zur Laufzeit den Cache ablehnt. Listing 10 zeigt eine kompakte Variante mit Eclipse Temurin als Basis.
Mehrstufiges Dockerfile mit Trainings-Stage
Listing 10: Mehrstufiges Dockerfile mit Build, Training und Runtime
FROM eclipse-temurin:25-jdk AS builder
WORKDIR /app
COPY . .
RUN ./mvnw -q -DskipTests package \
&& java -Djarmode=tools -jar target/aot-cache-demo.jar \
extract --destination extracted
FROM eclipse-temurin:25-jre AS trainer
WORKDIR /app
COPY --from=builder /app/extracted/ ./
RUN java -XX:AOTCacheOutput=app.aot \
-Dspring.context.exit=onRefresh \
-jar aot-cache-demo.jar
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=builder /app/extracted/ ./
COPY --from=trainer /app/app.aot ./
ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "aot-cache-demo.jar"]
In Kubernetes-Umgebungen empfiehlt es sich, den Cache direkt in das Container-Image einzubetten. So werden Volumes vermieden, deren Lebenszyklus von dem der Anwendung entkoppelt ist, und es wird sichergestellt, dass jede Image-Version ihren passenden Cache mitbringt. Werden Caches hingegen separat in einem Volume gehalten, muss bei Rollouts sehr genau über Versionierung und Konsistenz nachgedacht werden. Die einfachere und in den meisten Fällen bessere Lösung ist und bleibt: ein Cache pro Image, ein Image pro Version.
In CI/CD-Pipelines ist die Cache-Erzeugung als verpflichtender Build-Step zu führen. Cache-Artefakte sollten pro Architektur und JDK eindeutig getaggt werden – ein Cache für x64 funktioniert nicht auf aarch64 und umgekehrt. Bei jeder neuen Anwendungsversion entsteht ein neuer Cache. Ein Reuse auf Verdacht spart zwar wenige Sekunden Build-Zeit, kostet aber im Zweifelsfall aber Stunden für die Diagnose. Wer zusätzlich reproduzierbare JAR-Timestamps erzwingt – in der Maven-Welt am einfachsten über die Property project.build.outputTimestamp in der Datei pom.xml, die durch mvn artifact:check-buildplan geprüft wird –, schafft die Voraussetzung für stabile Caches über CI-Worker hinweg.
AOT-Cache im Vergleich zu CRaC und GraalVM
Es gibt nicht den einen richtigen Weg zu einem schnellen Java-Start-up. AOT-Cache, CRaC [11] und GraalVM Native Image [12] sind drei unterschiedliche Antworten auf dieselbe Frage, mit jeweils eigenen Stärken und Trade-offs. Die Auswahl sollte sich nach dem konkreten Workload, der Plattform und der organisatorischen Reife des Teams richten und nicht nach Trends.
CRaC (Coordinated Restore at Checkpoint) geht dabei den radikalsten Weg. Ein laufender Prozess wird per CRIU eingefroren und später wiederhergestellt. Der Restore ist extrem schnell, da Klassen, Caches und Heap bereits im stationären Zustand wiederhergestellt werden. Der Preis dafür liegt jedoch in der höheren Betriebskomplexität. CRaC erfordert Linux mit CRIU, eine bewusste Behandlung offener Datei-, Netzwerk- und Datenbank-Handles vor dem Checkpoint sowie einen Sicherheitsreview der Snapshot-Inhalte. Secrets, die zum Checkpoint-Zeitpunkt im Speicher liegen, landen ungewollt im Snapshot und müssen daher aktiv ausgeschlossen werden. Für Teams mit harten Start-up-Zielen und passender Plattform ist CRaC eine starke Option, für viele Standard-Workloads bleibt der Aufwand jedoch hoch.
GraalVM Native Image kompiliert eine Anwendung dagegen statisch zu einem nativen Binary. Das Resultat sind ein Start-up im Bereich von Millisekunden und ein deutlich reduzierter Footprint. Der Preis dafür ist die Closed-World-Assumption: Reflection, dynamische Proxies und Resource Loading müssen vorab konfiguriert werden, die Build-Zeiten sind länger und manche Bibliotheken erfordern explizite Kompatibilitätsarbeit. Für Serverless Functions, CLI-Tools und Microservices mit extremen Start-up-Anforderungen ist Native Image häufig die richtige Wahl. Für klassische Webanwendungen und APIs auf langlebigen Instanzen ist die JVM mit AOT-Cache meist flexibler und besser wartbar.
In dieser Landschaft positioniert sich der AOT-Cache als pragmatischer Mittelweg. Er erfordert keine neue Plattform, keine Closed-World-Konfiguration und keinen Snapshot-Mechanismus. Stattdessen bringt er einen klaren, reproduzierbaren Workflow mit, der sich gut in bestehende CI/CD-Strukturen einfügt. Für die meisten Enterprise-Teams stellt er den besten Kompromiss aus Wartbarkeit, Performance und Tooling dar. CRaC und Native Image bleiben auch weiterhin sinnvolle Spezialwerkzeuge für klar abgegrenzte Szenarien.
Wann lohnt sich AOT-Caching nicht?
Nicht alle Workloads profitieren von AOT-Caching. Kurzlebige CLI-Tools und Batch-Jobs, die nach wenigen Sekunden ohnehin beendet sind, profitieren kaum. Der Trainingslauf in der Pipeline kostet mehr Zeit, als der Cache zur Laufzeit jemals einspart. Ähnlich verhält es sich mit klassischen, langlebigen Monolithen, die nur einmal pro Quartal deployt werden. Hier ist CDS oft völlig ausreichend. Echte Serverless-Funktionen mit Sub-Sekunden-Anforderungen wiederum liegen jenseits dessen, was die JVM realistisch leisten kann. Hierfür bleibt GraalVM Native Image das passendere Werkzeug. Anwendungen, die zur Laufzeit massiv Klassen generieren (etwa über Bytecode-Manipulation auf jedem Request), erhalten durch den AOT-Cache zwar einen schnelleren Start, aber keinen Warm-up-Vorteil im späteren Lebenszyklus. Die Regel lautet: Ein AOT-Cache lohnt sich, wenn der Start-up-Pfad reproduzierbar und im Verhältnis zur Lebensdauer einer Instanz relevant ist.
Troubleshooting und Praxistipps
Die meisten Probleme rund um den AOT-Cache sind diagnostizierbar, sobald Logging, Metriken und Vergleichsläufe standardisiert sind. Wer mit den richtigen Werkzeugen systematisch vorgeht, findet in der Regel innerhalb weniger Minuten die Ursache. Tabelle 6 fasst die häufigsten Symptome und ihre wahrscheinliche Ursache zusammen.
| Symptom | Wahrscheinliche Ursache | Reaktion |
|---|---|---|
| AOT cache cannot be used | Classpath-Mismatch | Training- und Runtime-Classpath angleichen |
| Cache lädt, aber kein Speedup | Training nicht repräsentativ | kritische Endpunkte in Training aufnehmen |
| Cache-Datei sehr groß | Trainingsumfang zu breit | Mocks nutzen, Test-Frameworks ausschließen |
| ClassNotFoundException zur Laufzeit | Runtime-Classpath fehlt JARs | Classpath als Obermenge sicherstellen |
| Stiller Start ohne Cache | Cache-Datei nicht gefunden oder inkompatibel | strikten Modus aktivieren, Logs auswerten |
Tabelle 6: Typische Fehlerbilder und Erstreaktionen
Für die laufende Optimierung lohnen sich standardisierte Vergleichsmessungen. Ein einfacher Zeitvergleich zwischen einem Lauf ohne und einem Lauf mit Cache liefert eine erste belastbare Zahl. Eine zusätzliche JFR-Aufzeichnung der ersten 30 Sekunden zeigt, wo Hot Methods und GC-Aktivität liegen. So wird sichtbar, ob ein Cache nur die Klassenladung beschleunigt oder auch das Warm-up verkürzt. Listing 11 zeigt das passende Kommandoset dafür.
Listing 11: Vergleichsmessung ohne und mit Cache
time java -jar app.jar > /dev/null
time java -XX:AOTCache=app.aot -jar app.jar > /dev/null
java -XX:AOTCache=app.aot \
-XX:StartFlightRecording:settings=default,duration=30s,filename=profile.jfr \
-jar app.jar
jfr view hot-methods profile.jfr
jfr view gc profile.jfr
In der Praxis tauchen drei Stolpersteine besonders häufig auf. Erstens gibt es oft unterschiedliche JAR-Timestamps zwischen Training und Deployment, was häufig durch nicht reproduzierbare Builds ausgelöst wird. Zweitens: ein Deployment-Classpath, der weniger JARs enthält als das Training. Die Anforderung lautet hier, dass der Runtime-Classpath eine Obermenge sein muss. Und drittens: unbemerkt unterschiedliche JDK-Versionen oder Architekturen – etwa, wenn ein lokaler Build auf aarch64 entstand und auf x64-Worker deployt wird. Alle drei Punkte sollten in einer Pre-Deployment-Checkliste berücksichtigt werden, die als Gate vor dem Rollout fungiert.
Für das Cache-Tuning auf Anwendungsseite gelten drei einfache Regeln. Erstens ist der Cache schlank zu halten, indem Test- und Hilfsframeworks aus dem Training ferngehalten werden. Zweitens sollten reproduzierbare Build-Timestamps erzwungen werden, damit Cache-Inhalte über CI-Worker hinweg deterministisch werden. Und drittens müssen separate Caches für x64 und aarch64 gepflegt werden, statt einen universellen Cache zu erzwingen, den die JVM ohnehin verweigern würde.
Pre-Deployment-Checkliste für den AOT-Cache
-
JDK-Version und Vendor zwischen Training und Runtime identisch
-
CPU-Architektur (x64 / aarch64) zwischen Build- und Ziel-Stage identisch
-
JAR-Inhalte und -Timestamps zwischen Training und Deployment unverändert (project.build.outputTimestamp)
-
Runtime-Classpath ist Obermenge des Trainings-Classpaths, nur JAR-Dateien, keine Verzeichnisse
-
Trainingslauf ohne produktive Secrets, gegen Mocks oder Testcontainer
-
Strikter Modus in CI/Staging aktiviert, in Produktion stattdessen Logging und Metriken
-
Vergleichsmessung tready ohne und mit Cache dokumentiert
Stay tuned
Regelmäßig News zur Konferenz und der JAX-Community erhalten
Fazit
AOT-Caching ist im Jahr 2026 ein ausgereiftes Werkzeug. Mit JDK 24 bis 26 steht ein technisch belastbarer Pfad bereit, der die Stärken der JVM erhält und gleichzeitig Start-up- und Warm-up-Zeiten in cloud-nativen Betriebsmodellen spürbar reduziert. Im Unterschied zu Native Image bleibt die volle Java-Plattform-Kompatibilität erhalten und anders als bei CRaC fällt keine zusätzliche Linux- und Snapshot-Komplexität an. Damit ist AOT-Caching für viele Teams der pragmatischste nächste Schritt nach einer CDS-Baseline.
Ein nachhaltiger technischer Gewinn entsteht jedoch nur dann, wenn der Prozess diszipliniert umgesetzt wird. Die zentralen Punkte sind eindeutig: konsistente Artefaktstruktur, reproduzierbares Training, saubere Verifikation, verbindliche Pipeline-Schritte und ein bewusster Umgang mit Secrets. Wer diese Regeln einhält, gewinnt nicht nur an Geschwindigkeit beim Start, sondern auch an Vorhersagbarkeit im Betrieb. Kombinationen mit Spring AOT und reproduzierbaren Buildpacks verstärken diesen Effekt zusätzlich, ohne die zugrunde liegende Mechanik zu verändern.
Der praktikable Einstieg lässt sich in fünf Schritten zusammenfassen: Baseline messen, Extracted Layout etablieren, ersten Cache mit Exit-on-Refresh erzeugen, mit Logs und JFR validieren, Trainingsqualität schrittweise verbessern. Mit jedem JDK-Release wird das Verfahren leistungsfähiger. Method Profiling in JDK 25 und Object Caching mit beliebigem GC in JDK 26 sind dabei nur zwei Bausteine einer längeren Roadmap. Wer heute beginnt, profitiert automatisch mit jedem Upgrade. Genau das macht AOT-Caching zu einer Investition mit überschaubarem Aufwand und langem Wirkzeitraum.
Links und Literatur
[1] https://openjdk.org/projects/leyden/
[2] https://openjdk.org/jeps/483
[3] https://openjdk.org/jeps/514
[4] https://openjdk.org/jeps/515
[5] https://openjdk.org/jeps/516
[6] https://docs.spring.io/spring-boot/reference/packaging/aot-cache.html
[7] https://docs.spring.io/spring-framework/reference/core/aot.html
[8] https://docs.spring.io/spring-boot/reference/packaging/efficient.html
[9] https://paketo.io/docs/howto/java/
[10] https://docs.oracle.com/en/java/javase/21/vm/class-data-sharing.html
[11] https://openjdk.org/projects/crac/
[12] https://www.graalvm.org/latest/reference-manual/native-image/
Author
🔍 Frequently Asked Questions (FAQ)
1. Was ist AOT-Caching mit Project Leyden?
Project Leyden verlagert wiederholbare JVM-Arbeit aus dem Startpfad in einen kontrollierten Trainings- und Build-Kontext. Dabei werden Informationen über geladene und gelinkte Klassen sowie, ab JDK 25, Profiling-Daten heißer Methoden in einem AOT-Cache gespeichert. Beim späteren Start kann die JVM diese Informationen wiederverwenden, ohne ihre dynamischen Eigenschaften aufzugeben.
2. Wie beschleunigt Project Leyden den Start von Java-Anwendungen?
Der AOT-Cache reduziert Arbeit, die sonst bei jedem JVM-Start erneut ausgeführt werden müsste, beispielsweise beim Laden und Linken von Klassen. Dadurch können sowohl die Zeit bis zur Betriebsbereitschaft (t_ready) als auch der anschließende Warm-up bis zum stabilen Lastzustand (t_steady) sinken. Für Spring PetClinic beschreibt der Artikel beispielsweise eine Reduktion der Startzeit von rund drei auf etwa eine Sekunde durch CDS und AOT-Cache.
3. Wie unterscheidet sich Project Leyden von GraalVM Native Image und CRaC?
Project Leyden optimiert den Start innerhalb der klassischen JVM und behält deren dynamisches Verhalten bei. GraalVM Native Image erzeugt dagegen ein natives Binary mit sehr schnellem Kaltstart, verwendet dafür aber eine Closed-World-Annahme und benötigt zusätzliche Konfiguration für dynamische Java-Features. CRaC erreicht schnelle Restores über Prozess-Snapshots, bringt jedoch zusätzliche Linux-, CRIU- und Snapshot-Komplexität mit sich.
4. Wie funktioniert AOT-Caching mit Spring Boot?
Für Spring Boot sollte die Anwendung zunächst in ein extrahiertes Layout überführt werden, da ein klassisches Fat JAR für den AOT-Cache-Pfad ungünstig ist. Ein reproduzierbarer Trainingslauf kann anschließend mit spring.context.exit=onRefresh und -XX:AOTCacheOutput bis zum Context Refresh ausgeführt werden. In der Runtime wird der erzeugte Cache mit -XX:AOTCache=... geladen, ohne dass dafür Cache-spezifischer Anwendungscode erforderlich ist.
5. Welche Bedingungen müssen AOT-Training und Runtime erfüllen?
AOT-Caches sind eng an ihre technische Umgebung gebunden. Training und Runtime müssen unter anderem hinsichtlich JDK, Hardware-Architektur und Artefaktstruktur konsistent sein; außerdem sollten reproduzierbare JAR-Inhalte und Timestamps verwendet werden. Änderungen an Code, Dependencies, JDK oder Build-Artefakten erfordern deshalb einen neuen Cache.
6. Wann lohnt sich AOT-Caching für Java nicht?
AOT-Caching lohnt sich vor allem dann, wenn der Start-up-Pfad reproduzierbar und im Verhältnis zur Lebensdauer einer Instanz relevant ist. Sehr kurzlebige CLI- oder Batch-Anwendungen profitieren häufig zu wenig, während bei selten gestarteten, langlebigen Monolithen CDS ausreichen kann. Für Serverless-Anwendungen mit extremen Sub-Sekunden-Anforderungen nennt der Artikel GraalVM Native Image als passendere Alternative.





