Add jdk28-zgc: ZGC startup and warmup baseline on JDK 25/27/28-ea, and a probe for JEP 545/546

Spring Boot 4.1.1 harness measuring first-response time versus initial heap size and a
20,000-request warmup. Shows -XX:+ZAdaptiveHeapSizing is not yet in any build.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WzVQ6zFh9cuLPj9RSbRH8U
This commit is contained in:
2026-10-12 17:13:14 +00:00
co-authored by Claude Sonnet 5.5
parent 2f88734f5d
commit df13a904d7
11 changed files with 431 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/** Fires TOTAL requests with THREADS threads and reports how long each batch of BATCH took. */
public class Warmup {
public static void main(String[] a) throws Exception {
int port = Integer.parseInt(a[0]);
int total = 20_000, batch = 2_000, threads = 4;
HttpClient c = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
HttpRequest rq = HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/hello")).build();
AtomicInteger done = new AtomicInteger();
long[] batchEnd = new long[total / batch];
long t0 = System.nanoTime();
ExecutorService ex = Executors.newFixedThreadPool(threads);
List<Future<?>> fs = new ArrayList<>();
for (int t = 0; t < threads; t++) fs.add(ex.submit(() -> {
try {
while (true) {
int n = done.incrementAndGet();
if (n > total) return null;
c.send(rq, HttpResponse.BodyHandlers.discarding());
if (n % batch == 0) batchEnd[n / batch - 1] = System.nanoTime();
}
} catch (Exception e) { throw new RuntimeException(e); }
}));
for (Future<?> f : fs) f.get();
ex.shutdown();
StringBuilder sb = new StringBuilder("batch ms:");
long prev = t0;
for (long e : batchEnd) { sb.append(' ').append((e - prev) / 1_000_000); prev = e; }
System.out.println(sb);
System.out.println("total ms: " + (prev - t0) / 1_000_000);
}
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Set J25, J27, J28 to the three JDK home directories.
# Timing results drift between runs and machines; treat them as indicative, not exact.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
OUT="$HERE/../output"; mkdir -p "$OUT"
: "${J25:?}" "${J27:?}" "${J28:?}"
export JDK_CLIENT="$J25"
RUNS=${RUNS:-5}
# 01 - what does each JDK say about the ZGC flags the two JEPs talk about?
{
for p in "25|$J25" "27|$J27" "28-ea|$J28"; do
v=${p%%|*}; h=${p#*|}
echo "== JDK $v"
"$h/bin/java" -version 2>&1 | grep -v Picked | sed -n 1p
"$h/bin/java" -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -XX:+PrintFlagsFinal -version 2>&1 \
| grep -E ' (InitialHeapSize|MaxHeapSize|SoftMaxHeapSize|ZUncommit|ZUncommitDelay) ' | sed 's/ \{2,\}/ /g'
echo "-- probing -XX:+ZAdaptiveHeapSizing"
"$h/bin/java" -XX:+UseZGC -XX:+ZAdaptiveHeapSizing -version 2>&1 | grep -v Picked | sed -n 1p
echo
done
} > "$OUT/01-flag-probe.txt"
# 02 - startup versus initial heap size
{
for p in "jdk25|$J25" "jdk27|$J27" "jdk28ea|$J28"; do
l=${p%%|*}; h=${p#*|}
"$HERE/startup.sh" "$h" "$l zgc default" "$RUNS" -XX:+UseZGC
"$HERE/startup.sh" "$h" "$l zgc Xms64m" "$RUNS" -XX:+UseZGC -Xms64m
"$HERE/startup.sh" "$h" "$l zgc Xms1g" "$RUNS" -XX:+UseZGC -Xms1g
"$HERE/startup.sh" "$h" "$l zgc Xms3g" "$RUNS" -XX:+UseZGC -Xms3g
"$HERE/startup.sh" "$h" "$l zgc Xms3g pretouch" "$RUNS" -XX:+UseZGC -Xms3g -XX:+AlwaysPreTouch
done
} > "$OUT/02-startup-vs-xms.txt" 2>&1
# 03 - warmup: how long do the first 20,000 requests take?
{
for p in "jdk25|$J25" "jdk28ea|$J28"; do
l=${p%%|*}; h=${p#*|}
"$HERE/warmup.sh" "$h" "$l zgc default" -XX:+UseZGC
"$HERE/warmup.sh" "$h" "$l zgc Xms3g" -XX:+UseZGC -Xms3g
"$HERE/warmup.sh" "$h" "$l zgc Xms64m" -XX:+UseZGC -Xms64m
done
} > "$OUT/03-warmup.txt" 2>&1
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Usage: startup.sh <java-home> <label> <runs> [jvm args...]
# Starts the Spring Boot jar, polls /hello until HTTP 200, and prints wall-clock ms from
# process start to first response plus the committed heap one second later. One line per run,
# then a summary line (min / median / max).
set -u
JH=$1; LABEL=$2; RUNS=$3; shift 3
JAR="$(dirname "$0")/../target/jdk28-zgc-1.0.0.jar"
PORT=${PORT:-18080}
times=()
for r in $(seq 1 "$RUNS"); do
t0=$(date +%s%N)
"$JH/bin/java" "$@" -jar "$JAR" --server.port=$PORT --spring.main.banner-mode=off \
--logging.level.root=WARN > /dev/null 2>&1 &
PID=$!
while :; do
code=$(curl -s -o /dev/null -m 1 -w '%{http_code}' "localhost:$PORT/hello")
[ "$code" = 200 ] && break
kill -0 $PID 2>/dev/null || { echo "$LABEL run $r: JVM exited early"; break; }
sleep 0.02
done
t1=$(date +%s%N)
ms=$(( (t1 - t0) / 1000000 ))
sleep 1
committed=$(curl -s "localhost:$PORT/heap" | sed -n 's/.*"committedMiB":\([0-9]*\).*/\1/p')
echo "$LABEL run $r: first-response ${ms} ms, committed heap ${committed} MiB"
times+=("$ms")
kill $PID 2>/dev/null; wait $PID 2>/dev/null
done
sorted=($(printf '%s\n' "${times[@]}" | sort -n))
n=${#sorted[@]}
echo "$LABEL SUMMARY: min ${sorted[0]} ms, median ${sorted[$((n/2))]} ms, max ${sorted[$((n-1))]} ms (n=$n)"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Usage: warmup.sh <java-home> <label> [jvm args...]
# Starts the server, waits for the first response, then runs Warmup.java (always on the same
# client JDK, JDK_CLIENT) so only the server JVM changes between runs.
set -u
JH=$1; LABEL=$2; shift 2
HERE="$(cd "$(dirname "$0")" && pwd)"
JAR="$HERE/../target/jdk28-zgc-1.0.0.jar"
PORT=${PORT:-18080}
CLIENT=${JDK_CLIENT:-$JH}
"$JH/bin/java" "$@" -jar "$JAR" --server.port=$PORT --spring.main.banner-mode=off \
--logging.level.root=WARN > /dev/null 2>&1 &
PID=$!
until [ "$(curl -s -o /dev/null -m 1 -w '%{http_code}' localhost:$PORT/hello)" = 200 ]; do sleep 0.02; done
echo "== $LABEL"
"$CLIENT/bin/java" "$HERE/Warmup.java" $PORT 2>&1 | grep -v Picked
curl -s "localhost:$PORT/heap" | sed -n 's/.*"committedMiB":\([0-9]*\),"maxMiB":\([0-9]*\).*/committed after load: \1 MiB (max \2 MiB)/p'; echo
kill $PID 2>/dev/null; wait $PID 2>/dev/null