Files
spring-boot-demo/docker-images/docs/06-aot-cache.md
T
asmhatreandClaude Opus 5 644da9e65e Add docker-images: one Spring Boot 4 service packaged nine ways
Companion code for "Dockerizing Spring Boot 4: Layered Jars, Buildpacks,
Distroless and Image Size Benchmarks". Fat jar on JDK and JRE, layered jar
on Debian, Alpine and distroless, jlink, the JDK 25 AOT cache, Paketo
buildpacks and Jib, each measured for size on disk and pushed, rebuild
delta, startup, user and shell. Also PID 1 and signal handling, the jdeps
module gap, AOT cache mismatches and buildpacks memory calculation.
Transcripts in docs/output/, regenerated by scripts/run-all.sh.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:11:46 +00:00

2.7 KiB

6. The JDK 25 AOT cache

← 5. jlink · Index · Next: 7. PID 1 and signals →

JDK 24 added the AOT cache (JEP 483: classes loaded and linked ahead of time), and JDK 25 made it one step (JEP 514, -XX:AOTCacheOutput) and added method profiles to it (JEP 515). Spring Boot's documented Dockerfile does a training run at image build time:

RUN java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar application.jar
ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "application.jar"]

spring.context.exit=onRefresh starts the context and exits, so the training run needs no database or network - unless your beans touch them during startup.

What it buys and what it costs

layered-jre aot-cache
Started in (median of 3) 3.356 s 1.638 s
on disk 377 MB 440 MB (+63 MB)
pushed after a one-line change 6 KB 15.3 MB

The last row is the one nobody mentions. The training run happens after the application layer is copied, so every code change invalidates it and the cache layer is rebuilt and re-pushed. Half the startup time costs back most of what layering saved on pushes. Whether that trade is worth it depends on how often pods start versus how often you deploy - scale-to-zero and aggressive autoscaling say yes; a fleet that deploys twenty times a day and rarely restarts says no.

A cache from a different JVM

The cache is only valid for the exact JVM build that wrote it. Dockerfile.aot-cache-mismatch copies the trained application onto the distroless image, whose JVM is Temurin 25.0.4.1 instead of 25.0.4 (aot-cache-mismatch.txt):

[0.007s][warning][aot] The AOT cache was created by a different version or build of HotSpot
[0.007s][error  ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
[0.008s][error  ][aot] Loading static archive failed.
[0.008s][error  ][aot] Unable to map shared spaces
Starting ImagesApplication v1.0.0 using Java 25.0.4.1 with PID 1 (/application/application.jar started by nonroot in /ap
Started ImagesApplication in 3.564 seconds (process running for 4.05)

Four lines at error level, and then the application starts normally without the cache - back at 3.5 s. Nothing fails a health check. The way this happens in practice is a floating base-image tag (25-jre) moving to a new JVM build between the stage that trained the cache and a later rebuild that reused a cached training layer - or a multi-stage build that trains on one image and runs on another, as here. Train and run on the same image, pin it by digest, and alert on [aot] lines at error.