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:
143
scripts/run-all.ps1
Normal file
143
scripts/run-all.ps1
Normal file
@@ -0,0 +1,143 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Compiles and runs the whole ZGC-vs-G1 suite, writing every raw output into results/.
|
||||
|
||||
.DESCRIPTION
|
||||
Every number published in the companion blog post comes from this script. It is deliberately
|
||||
boring and sequential: GC benchmarks that run concurrently with each other measure the scheduler.
|
||||
|
||||
.PARAMETER JavaHome
|
||||
Path to a JDK 25 (or newer) installation. Generational ZGC is the only ZGC from JDK 24 onward.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh scripts/run-all.ps1 -JavaHome 'C:\Program Files\Java\jdk-25.0.3'
|
||||
#>
|
||||
param(
|
||||
[string]$JavaHome = $env:JAVA_HOME,
|
||||
[string]$MavenHome = "$HOME\ankurm-blog-tools\apache-maven-3.9.9",
|
||||
[int]$MeasureSeconds = 60,
|
||||
[int]$WarmupSeconds = 20,
|
||||
[switch]$SkipJmh
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not $JavaHome) { throw "Set -JavaHome or JAVA_HOME to a JDK 25+ installation." }
|
||||
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$Java = Join-Path $JavaHome 'bin\java.exe'
|
||||
$Javac = Join-Path $JavaHome 'bin\javac.exe'
|
||||
$Out = Join-Path $Root 'out'
|
||||
$Results = Join-Path $Root 'results'
|
||||
$Logs = Join-Path $Root 'logs'
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Out, $Results, $Logs | Out-Null
|
||||
|
||||
# Fixed heap for every run. -Xms == -Xmx removes heap resizing as a variable; a benchmark that lets
|
||||
# the heap grow during measurement is partly measuring the resize policy.
|
||||
$Heap = @('-Xms2g', '-Xmx2g')
|
||||
|
||||
Write-Host "==> compiling" -ForegroundColor Cyan
|
||||
$sources = Get-ChildItem "$Root\latency\src", "$Root\tuning\src", "$Root\internals\src", "$Root\analysis\src" `
|
||||
-Recurse -Filter *.java -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
|
||||
& $Javac -d $Out @sources
|
||||
if ($LASTEXITCODE -ne 0) { throw "compilation failed" }
|
||||
|
||||
# NOTE: several of these commands (notably `java -version` and `-XX:+PrintFlagsFinal`) write to
|
||||
# stderr. PowerShell turns native stderr into error records, and with $ErrorActionPreference='Stop'
|
||||
# that aborts the whole script on a perfectly successful run. Routing through cmd.exe with a plain
|
||||
# 2>&1 merge keeps stderr as text where it belongs.
|
||||
function Invoke-Capture {
|
||||
param([string]$Name, [string[]]$JvmArgs, [string[]]$AppArgs)
|
||||
$file = Join-Path $Results "$Name.txt"
|
||||
Write-Host "==> $Name" -ForegroundColor Cyan
|
||||
$quoted = @("`"$Java`"") + ($JvmArgs + $AppArgs | ForEach-Object {
|
||||
if ($_ -match '[\s]') { "`"$_`"" } else { $_ }
|
||||
})
|
||||
$line = ($quoted -join ' ')
|
||||
$output = cmd /c "$line 2>&1"
|
||||
$output | Out-File -FilePath $file -Encoding utf8
|
||||
$output | Select-Object -Last 6 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||
Write-Host " -> results/$Name.txt" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# 1. Environment -- printed once per collector so the results file is self-describing.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
Invoke-Capture -Name '01-env-zgc' -JvmArgs (@('-XX:+UseZGC') + $Heap) -AppArgs @("$Root\env\Env.java")
|
||||
Invoke-Capture -Name '01-env-g1' -JvmArgs (@('-XX:+UseG1GC') + $Heap) -AppArgs @("$Root\env\Env.java")
|
||||
|
||||
Invoke-Capture -Name '02-flags-zgc' -JvmArgs (@('-XX:+UseZGC') + $Heap + @('-XX:+PrintFlagsFinal')) -AppArgs @('-version')
|
||||
Invoke-Capture -Name '02-flags-g1' -JvmArgs (@('-XX:+UseG1GC') + $Heap + @('-XX:+PrintFlagsFinal')) -AppArgs @('-version')
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# 2. Open-loop latency, with full unified GC logging written alongside.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
$latencyProps = @("-DwarmupSeconds=$WarmupSeconds", "-Dseconds=$MeasureSeconds")
|
||||
|
||||
Invoke-Capture -Name '03-latency-zgc' `
|
||||
-JvmArgs (@('-XX:+UseZGC') + $Heap + $latencyProps + @("-Xlog:gc*,gc+heap=debug:file=$Logs\zgc-latency.log:time,uptime,level,tags")) `
|
||||
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.latency.LatencyHarness')
|
||||
|
||||
Invoke-Capture -Name '03-latency-g1' `
|
||||
-JvmArgs (@('-XX:+UseG1GC') + $Heap + $latencyProps + @("-Xlog:gc*,gc+heap=debug:file=$Logs\g1-latency.log:time,uptime,level,tags")) `
|
||||
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.latency.LatencyHarness')
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# 3. Allocation stalls -- deliberately undersized heap. This is the section most GC posts skip.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
$stallHeap = @('-Xms512m', '-Xmx512m')
|
||||
|
||||
Invoke-Capture -Name '04-stalls-zgc-baseline' `
|
||||
-JvmArgs (@('-XX:+UseZGC') + $stallHeap + @('-Dseconds=20')) `
|
||||
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||
|
||||
Invoke-Capture -Name '04-stalls-zgc-softmax' `
|
||||
-JvmArgs (@('-XX:+UseZGC', '-XX:SoftMaxHeapSize=400m') + $stallHeap + @('-Dseconds=20')) `
|
||||
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||
|
||||
Invoke-Capture -Name '04-stalls-zgc-spiketolerance' `
|
||||
-JvmArgs (@('-XX:+UseZGC', '-XX:ZAllocationSpikeTolerance=5') + $stallHeap + @('-Dseconds=20')) `
|
||||
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||
|
||||
# The same stress under G1: ZGC's stalls become G1's evacuation failures and Full GCs.
|
||||
Invoke-Capture -Name '04-stalls-g1' `
|
||||
-JvmArgs (@('-XX:+UseG1GC') + $stallHeap + @('-Dseconds=20', "-Xlog:gc:file=$Logs\g1-stress.log:time,uptime")) `
|
||||
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# 4. Page-size / humongous behaviour.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
Invoke-Capture -Name '05-pages-zgc' `
|
||||
-JvmArgs (@('-XX:+UseZGC') + $Heap) -AppArgs @('-cp', $Out, 'com.ankurm.zgc.internals.PageSizeDemo')
|
||||
Invoke-Capture -Name '05-pages-g1' `
|
||||
-JvmArgs (@('-XX:+UseG1GC') + $Heap) -AppArgs @('-cp', $Out, 'com.ankurm.zgc.internals.PageSizeDemo')
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# 5. GC log analysis -- turns the raw unified logs above into pause distributions.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
Invoke-Capture -Name '06-gclog-zgc' -JvmArgs @() -AppArgs @('-cp', $Out, 'com.ankurm.zgc.analysis.GcLogParser', "$Logs\zgc-latency.log")
|
||||
Invoke-Capture -Name '06-gclog-g1' -JvmArgs @() -AppArgs @('-cp', $Out, 'com.ankurm.zgc.analysis.GcLogParser', "$Logs\g1-latency.log")
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# 6. JMH throughput / barrier microbenchmarks.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
if (-not $SkipJmh) {
|
||||
Write-Host "==> building JMH module" -ForegroundColor Cyan
|
||||
Push-Location "$Root\jmh"
|
||||
$env:JAVA_HOME = $JavaHome
|
||||
& (Join-Path $MavenHome 'bin\mvn.cmd') -q -B clean package
|
||||
Pop-Location
|
||||
|
||||
foreach ($gc in @('UseZGC', 'UseG1GC')) {
|
||||
$tag = if ($gc -eq 'UseZGC') { 'zgc' } else { 'g1' }
|
||||
# IMPORTANT: the GC flag must go in -jvmArgs. JMH forks a fresh JVM for every trial, and a
|
||||
# GC flag on the outer `java -jar` command is NOT inherited by the fork. Getting this wrong
|
||||
# is the single most common way to publish a GC benchmark that measured the default GC twice.
|
||||
Invoke-Capture -Name "07-jmh-$tag" -JvmArgs @('-jar', "$Root\jmh\target\benchmarks.jar") `
|
||||
-AppArgs @('-jvmArgs', "-XX:+$gc -Xms2g -Xmx2g", '-prof', 'gc', '-rf', 'json',
|
||||
'-rff', "$Results\07-jmh-$tag.json")
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "All raw output is in $Results" -ForegroundColor Green
|
||||
89
scripts/run-all.sh
Normal file
89
scripts/run-all.sh
Normal 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"
|
||||
Reference in New Issue
Block a user