Files
zgc-jdk25-benchmarks/shenandoah-jdk28/README.md
T

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.