Add aot-cache module: the JDK 25 AOT cache on Spring Boot 4.1 vs AppCDS, Spring AOT and GraalVM native

A Spring Boot 4.1.1 order service started 12 ways (plain and extracted jar, Spring AOT output, AppCDS, AOT cache trained with and without traffic, JDK 27, native image), ten interleaved rounds each, with first-request latency; 24 cache-mismatch cases; Docker layer arithmetic. Transcripts are in output/ (no docs/ folder).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Claude
2026-09-24 16:44:16 +00:00
parent fa4205f631
commit 3e2029ab3a
30 changed files with 1023 additions and 1 deletions
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# What happens when the cache does not match the run. Each case starts the service with a cache that was made
# under different conditions, and prints (1) what the JVM logged about the cache, (2) how long start-up took.
# -XX:AOTMode=auto is the default: use the cache if it is valid, otherwise carry on without it.
# -XX:AOTMode=on : refuse to start if the cache cannot be used.
source "$(dirname "$0")/lib.sh"
J25=$JDK25/bin/java; J27=$JDK27/bin/java
run_case() { # title, workdir, cmd... (runs once with -Xlog:aot=warning so the cache's complaints show up)
local title=$1 wd=$2; shift 2
local t0 log="$WORK/mismatch.log"
echo "== $title"
t0=$(now_ms)
start_app "$wd" "$log" "$@" --server.port=$PORT
if wait_ready; then
echo " result: started, ready after $(( $(now_ms) - t0 )) ms ($(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$log"))"
stop_app
else
wait "$APP_PID" 2>/dev/null
echo " result: did not start (exit code $?)"
fi
grep -E '\[(warning|error)[ ]*\]\[(aot|cds)|saved state of|created with' "$log" | grep -v 'Skipping' | head -5 | sed -E 's/^\[[0-9.]+s\]/ log: /' | cut -c1-170
[ -z "$(grep -E '\[(warning|error)[ ]*\]\[(aot|cds)' "$log" | grep -v Skipping)" ] && echo " log: (nothing at warning or error level)"
echo
}
echo "# cache: $WORK/refresh.aot, trained on JDK 25.0.4.1, extracted layout, default GC (G1), 2 CPUs"
echo
run_case "control: the same JVM, the same jar, the same path" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
# 1. the jar changed after training
# (cp -a everywhere below: plain cp gives every file a new modification time, which the cache checks - see the timestamp case)
rm -rf "$WORK/ext-changed"; cp -a "$WORK/ext" "$WORK/ext-changed"
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --destination "$WORK/changed-tmp" >/dev/null
cp "$WORK/changed-tmp/app-changed.jar" "$WORK/ext-changed/app.jar"; rm -rf "$WORK/changed-tmp" # only the application jar differs
run_case "the application jar was rebuilt after a one-line code change, default AOTMode=auto" "$WORK/ext-changed" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "the same, with -XX:AOTMode=on" "$WORK/ext-changed" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
# 2. a dependency jar changed
rm -rf "$WORK/ext-dep"; cp -a "$WORK/ext" "$WORK/ext-dep"
( cd "$WORK/ext-dep/lib" && f=$(ls spring-core-*.jar) && touch -d '2001-01-01' "$f" )
run_case "a dependency jar has a different modification time (same bytes)" "$WORK/ext-dep" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
# 2b. only a copy, with fresh timestamps (what `cp -r` or a re-extract does)
rm -rf "$WORK/ext-copy"; cp -r "$WORK/ext" "$WORK/ext-copy"
run_case "the same files copied with plain cp (bytes and path layout identical, timestamps new)" "$WORK/ext-copy" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
# 3. moved to another directory
rm -rf "$WORK/moved"; mkdir -p "$WORK/moved"; cp -a "$WORK/ext" "$WORK/moved/app-dir"
run_case "the application directory was moved after training (same bytes, different path)" "$WORK/moved/app-dir" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
# 4. fat jar cache used with the extracted layout
run_case "cache trained on the fat jar, run from the extracted layout" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/fat.aot" -jar app.jar
# 5. different JVM
run_case "JDK 25's cache used by JDK 27" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "the same, with -XX:AOTMode=on" "$WORK/ext" "$J27" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
# 6. flags that change what the cache holds
run_case "-XX:+UseCompactObjectHeaders added at run time only (JDK 25)" "$WORK/ext" "$J25" -XX:+UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "a different collector at run time: -XX:+UseSerialGC" "$WORK/ext" "$J25" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "a different collector at run time: -XX:+UseParallelGC" "$WORK/ext" "$J25" -XX:+UseParallelGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "a different collector at run time: -XX:+UseZGC" "$WORK/ext" "$J25" -XX:+UseZGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "a different heap size at run time: -Xmx256m" "$WORK/ext" "$J25" -Xmx256m -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
run_case "a cache file that does not exist" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
run_case "a cache file that does not exist, with -XX:AOTMode=on" "$WORK/ext" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
echo "# and on JDK 27, whose cache was trained under the default collector (G1):"
echo
run_case "JDK 27, cache from JDK 27 (control)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
run_case "JDK 27, -XX:+UseZGC at run time" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
run_case "JDK 27, -XX:+UseSerialGC at run time" "$WORK/ext" "$J27" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
run_case "JDK 27, -XX:-UseCompactObjectHeaders at run time" "$WORK/ext" "$J27" -XX:-UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
echo "# why ZGC is different: ask the JVM (-Xlog:aot=info) why it refused the G1-trained cache"
echo
run_case "JDK 27, -XX:+UseZGC with the G1-trained cache, -Xlog:aot=info" "$WORK/ext" "$J27" -XX:+UseZGC -Xlog:aot=info -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
run_case "JDK 27, cache trained under ZGC, run under ZGC" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
run_case "JDK 27, cache trained under ZGC, run under G1 (compressed oops on again)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
run_case "JDK 27, cache trained under ZGC, run under G1 with -XX:-UseCompressedOops" "$WORK/ext" "$J27" -XX:-UseCompressedOops -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar