1
0
Files
zgc-jdk25-benchmarks/scripts/run-all.ps1
Ankur e189da79ba 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
2026-07-31 08:47:16 +05:30

144 lines
7.6 KiB
PowerShell

<#
.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