Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HmvoHH3ZnCEaPNXRpDxDZe
4.1 KiB
shenandoah-jdk28 — generational Shenandoah vs SATB Shenandoah vs G1 vs ZGC on a Spring Boot service
Companion module for the ankurm.com post
Generational Shenandoah by Default in JDK 28 (JEP 535): Benchmarked Against G1 and ZGC.
There is no docs/ folder: this README and the captured files in output/ are the documentation.
The honest status
JEP 535 ("Shenandoah: Generational Mode by Default", which also deprecates the SATB mode) is Targeted to JDK 28 on
openjdk.org, but it is not integrated in Temurin 28+21 EA. output/01-gc-mode-probe.txt
shows -XX:+UseShenandoahGC still starts in Snapshot-At-The-Beginning (SATB) mode on both JDK 25.0.5+8 and 28+21.
Generational mode is available on both as an explicit opt-in (-XX:ShenandoahGCMode=generational).
So this module benchmarks explicit generational vs explicit SATB, which is what the default will switch between.
Oracle's jdk.java.net builds do not contain Shenandoah at all; use Temurin (or another build that ships it).
Versions
| Component | Version |
|---|---|
| JDK 25 | Temurin 25.0.5+8 (probe only) |
| JDK 28 | Temurin 28+21 EA (28-beta+21-ea, build 202610221655) |
| Spring Boot | 4.1.2 (spring-boot-starter-webmvc) |
| Machine | 2 vCPU, 8 GB RAM Linux sandbox. A small box: absolute numbers are illustrative |
| Heap | -Xms1g -Xmx1g for every configuration |
Quick start
mvn -B -DskipTests package
export J25=/path/to/temurin-25 J28=/path/to/temurin-28-ea
scripts/run-all.sh # probe + 3 interleaved reps of 4 configurations, ~2.5 min per run
scripts/more-reps.sh 4 5 # append more reps and re-summarise
What is measured
| File | What it is |
|---|---|
src/main/java/com/ankurm/shenandoah/ShenandoahApplication.java |
/work replaces 8 of 200,000 1 KiB cache slots and allocates 100 short-lived 1 KiB arrays; /info reports the heap |
scripts/LoadGen.java |
open: fixed-rate load (3,000 req/s, 64 virtual threads), latency measured from the intended send time; closed: peak throughput |
scripts/bench-one.sh |
One run: 30 s warm-up, 60 s open-loop window, 20 s closed loop; pauses from -Xlog:gc*; peak RSS from VmHWM |
scripts/gc_pauses.py |
Stop-the-world pauses inside the measure window |
scripts/summarize.py |
Median per configuration |
scripts/log-events.sh |
Counts Cancelling GC, Pause Full, degenerated pauses and allocation failures per run; output is output/04-gc-events.txt |
Results (median of 5 interleaved runs, JDK 28+21 EA)
output/03-summary.txt, from the per-run lines in output/02-runs.txt:
| Configuration | p50 ms | p99 ms | p99.9 ms | max ms | peak req/s | pause max ms | RSS MB |
|---|---|---|---|---|---|---|---|
| G1 | 0.63 | 29.2 | 128.1 | 179.5 | 8,900 | 111.6 | 1,205 |
| ZGC (generational) | 0.60 | 7.4 | 17.3 | 36.4 | 7,117 | 0.17 | 1,250 |
| Shenandoah SATB | 0.78 | 14.9 | 43.6 | 86.4 | 5,026 | 1.5 | 1,212 |
| Shenandoah generational | 0.82 | 1,557.5 | 1,820.4 | 1,864.7 | 5,498 | 2.6 | 1,227 |
Read the last row carefully. Per-run p99 for generational Shenandoah was 1,557, 3,077, 24.5, 1,658 and 692 ms: four of five
runs contain a multi-second latency episode while the recorded stop-the-world pauses stay at a few milliseconds
(one run had a 209 ms Pause Full). output/04-gc-events.txt shows no allocation failures and no degenerated pauses in any run, and
the Cancelling GC: Concurrent GC lines are old-generation marking being preempted for a young collection, which is normal.
So the cause of the stalls is not established here. One SATB run (811 ms p99) also stalled with no GC event at all,
which says this 2-vCPU machine produces stalls of its own. Treat the generational result as "did not behave on this box
and this workload", not as a general verdict. A larger machine and a bigger heap are the obvious next experiment.