1
0

Generational ZGC vs G1 on JDK 25: benchmarks, internals and captured results

Complete companion suite for the ankurm.com guide. Everything was compiled and
executed on java 25.0.3+9-LTS-195 (AMD Ryzen 5 5600U, Windows 11); every file
under results/ is unedited program output.

Code
  latency/    open-loop latency harness that measures from intended arrival, so
              coordinated omission is accounted for rather than hidden. Reports
              response time and service time side by side; the gap reached 2910x
              on G1.
  jmh/        four microbenchmarks isolating one mechanism each: TLAB allocation,
              the ZGC load barrier (with a primitive-load control), the write
              barrier (with null / old-to-old / primitive controls), and promotion
              pressure.
  tuning/     provokes ZGC allocation stalls and reads its own JFR recording back,
              grouped by page class. jdk.ZAllocationStall is enabled without a
              threshold, because the 10 ms default hides most stalls.
  internals/  ZGC page classes vs G1 humongous, read from the live VM via
              HotSpotDiagnosticMXBean and jdk.ZPageAllocation events.
  analysis/   unified GC log parser that keeps stop-the-world pauses and
              concurrent phases in separate buckets.
  env/        environment capture; proves generational mode from JMX bean names.

Docs
  Eight chapters covering the JEP 439/474/490 timeline, colored pointers and both
  barriers, allocation stalls, a full flag reference, logging and JFR, benchmark
  methodology, corner cases, and a decision procedure.

Headline result (60 s, 20k req/s, 2 GB heap, ~585 MB live)
  p50               ZGC 0.006 ms   G1 0.005 ms
  p99.9             ZGC 1.437 ms   G1 95.169 ms
  total STW time    ZGC 1.503 ms   G1 1,139.624 ms
This commit is contained in:
2026-07-31 08:47:16 +05:30
commit e189da79ba
45 changed files with 12572 additions and 0 deletions

89
scripts/run-all.sh Normal file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Linux / macOS equivalent of run-all.ps1.
#
# ./scripts/run-all.sh /path/to/jdk-25 [measureSeconds] [warmupSeconds]
#
# Everything published in the companion blog post came from the PowerShell version on Windows;
# this script produces the same set of files in results/ so the comparison can be repeated on
# server hardware, which is where you should actually make a decision.
set -euo pipefail
JAVA_HOME_ARG="${1:-${JAVA_HOME:-}}"
MEASURE_SECONDS="${2:-60}"
WARMUP_SECONDS="${3:-20}"
if [[ -z "$JAVA_HOME_ARG" ]]; then
echo "usage: $0 /path/to/jdk-25 [measureSeconds] [warmupSeconds]" >&2
exit 2
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
JAVA="$JAVA_HOME_ARG/bin/java"
JAVAC="$JAVA_HOME_ARG/bin/javac"
OUT="$ROOT/out"
RESULTS="$ROOT/results"
LOGS="$ROOT/logs"
mkdir -p "$OUT" "$RESULTS" "$LOGS"
# Fixed heap: -Xms == -Xmx removes heap resizing as a variable.
HEAP=(-Xms2g -Xmx2g)
STALL_HEAP=(-Xms512m -Xmx512m)
echo "==> compiling"
mapfile -t SOURCES < <(find "$ROOT/latency/src" "$ROOT/tuning/src" "$ROOT/internals/src" "$ROOT/analysis/src" -name '*.java')
"$JAVAC" -d "$OUT" "${SOURCES[@]}"
capture() {
local name="$1"; shift
echo "==> $name"
"$@" > "$RESULTS/$name.txt" 2>&1 || true
tail -n 4 "$RESULTS/$name.txt" | sed 's/^/ /'
}
capture 01-env-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" "$ROOT/env/Env.java"
capture 01-env-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" "$ROOT/env/Env.java"
capture 02-flags-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" -XX:+PrintFlagsFinal -version
capture 02-flags-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" -XX:+PrintFlagsFinal -version
capture 03-latency-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" \
"-DwarmupSeconds=$WARMUP_SECONDS" "-Dseconds=$MEASURE_SECONDS" \
"-Xlog:gc*,gc+heap=debug:file=$LOGS/zgc-latency.log:time,uptime,level,tags" \
-cp "$OUT" com.ankurm.zgc.latency.LatencyHarness
capture 03-latency-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" \
"-DwarmupSeconds=$WARMUP_SECONDS" "-Dseconds=$MEASURE_SECONDS" \
"-Xlog:gc*,gc+heap=debug:file=$LOGS/g1-latency.log:time,uptime,level,tags" \
-cp "$OUT" com.ankurm.zgc.latency.LatencyHarness
capture 04-stalls-zgc-baseline "$JAVA" -XX:+UseZGC "${STALL_HEAP[@]}" -Dseconds=20 \
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
capture 04-stalls-zgc-softmax "$JAVA" -XX:+UseZGC -XX:SoftMaxHeapSize=400m "${STALL_HEAP[@]}" -Dseconds=20 \
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
capture 04-stalls-zgc-spiketolerance "$JAVA" -XX:+UseZGC -XX:ZAllocationSpikeTolerance=5 "${STALL_HEAP[@]}" -Dseconds=20 \
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
capture 04-stalls-g1 "$JAVA" -XX:+UseG1GC "${STALL_HEAP[@]}" -Dseconds=20 \
"-Xlog:gc:file=$LOGS/g1-stress.log:time,uptime" \
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
capture 05-pages-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" -cp "$OUT" com.ankurm.zgc.internals.PageSizeDemo
capture 05-pages-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" -cp "$OUT" com.ankurm.zgc.internals.PageSizeDemo
capture 06-gclog-zgc "$JAVA" -cp "$OUT" com.ankurm.zgc.analysis.GcLogParser "$LOGS/zgc-latency.log"
capture 06-gclog-g1 "$JAVA" -cp "$OUT" com.ankurm.zgc.analysis.GcLogParser "$LOGS/g1-latency.log"
echo "==> building JMH module"
(cd "$ROOT/jmh" && JAVA_HOME="$JAVA_HOME_ARG" mvn -q -B clean package)
# NOTE: the GC flag MUST be inside -jvmArgs. JMH forks a fresh JVM per trial and does not inherit
# flags from the outer `java -jar` command. See docs/06-methodology.md section 6.6.
for gc in UseZGC UseG1GC; do
tag=$([[ "$gc" == "UseZGC" ]] && echo zgc || echo g1)
capture "07-jmh-$tag" "$JAVA" -jar "$ROOT/jmh/target/benchmarks.jar" \
-jvmArgs "-XX:+$gc -Xms2g -Xmx2g" -prof gc -rf json -rff "$RESULTS/07-jmh-$tag.json"
done
echo
echo "All raw output is in $RESULTS"