Live heap, throughput, p99 and startup across four configurations, including opt-in and opt-out of compact object headers. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WzVQ6zFh9cuLPj9RSbRH8U
3.6 KiB
jdk27-perf — what actually got faster in JDK 27, measured on one Spring Boot service
Companion module for the ankurm.com post What Actually Got Faster in JDK 27: Measured on a Spring Boot App
(https://ankurm.com/what-actually-got-faster-in-jdk-27-measured-spring-boot/).
No docs/ folder: this README and the raw files in output/ are the documentation.
Question
JDK 27 changed two defaults that matter to a server: compact object headers are on (JEP 534) and G1 is the default collector everywhere (JEP 523). Does the same Spring Boot service get faster, smaller, or neither when you move from JDK 25 to JDK 27 with no flags?
Setup
| Thing | Value |
|---|---|
| JDK 25 | Temurin 25.0.4.1+1 (LTS) |
| JDK 27 | Temurin 27+35 (GA 2026-09-15) |
| Spring Boot | 4.1.1, spring-boot-starter-webmvc |
| Machine | 2 vCPU, 8 GB RAM Linux sandbox (the load client shares the box, so absolute numbers are modest and noisy) |
| Service | 3,000,000 resident Item(int id, int qty) records; /quote?n=200 builds 200 response records; Jackson serialises them |
| Load | closed loop, 4 client threads, 15 s warmup discarded, 30 s measured, client always JDK 25 |
| Repetitions | 3 per configuration, interleaved so machine drift hits all four equally |
Four configurations: jdk25 defaults, jdk25 +CompactObjectHeaders (opt in), jdk27 defaults, jdk27 -CompactObjectHeaders (opt out).
output/01-defaults.txt shows both JDKs pick G1 on this machine, so the collector is the same everywhere and only headers and the rest of the JDK differ.
Quick start
mvn -B -DskipTests package # build with JDK 25 or later
export J25=/path/to/jdk-25 J27=/path/to/jdk-27
scripts/run-all.sh # about 12 minutes; regenerates output/
REPS=5 scripts/run-all.sh # more repetitions
Results
Medians of 3 runs (output/03-summary.txt, raw blocks in output/02-runs.txt):
| Config | Startup ms | Throughput req/s (range) | p99 ms |
|---|---|---|---|
| jdk25 defaults | 3437 | 6788 (6239-6868) | 3.23 |
| jdk25 +CompactObjectHeaders | 3512 | 7157 (6407-7228) | 3.09 |
| jdk27 defaults | 3220 | 7016 (6755-7275) | 2.99 |
| jdk27 -CompactObjectHeaders | 3388 | 7010 (6867-7227) | 3.03 |
Resident data set after a forced full GC (output/04-live-set.txt, identical in all three repetitions):
| Config | Live heap MiB |
|---|---|
| jdk25 defaults | 93 |
| jdk25 +CompactObjectHeaders | 69 |
| jdk27 defaults | 70 |
| jdk27 -CompactObjectHeaders | 93-94 |
What it says
- Footprint is the clear win. Compact headers cut the live heap by 23 to 24 MiB (whole-MiB rounding). That matches 3,000,000 objects times 8 bytes saved, which is 22.9 MiB. A two-int record goes from 24 to 16 bytes.
- Speed is not distinguishable. The throughput ranges overlap in every row and p99 differs by fractions of a millisecond. On this box I cannot claim JDK 27 is faster.
- Startup is inconclusive. JDK 27 defaults had the lowest median, but the individual runs overlap with JDK 25.
- Resident set size and committed heap vary run to run with G1's sizing decisions; see the per-run
/statslines in02-runs.txtbefore drawing a conclusion from them.
Honest notes
output/02-runs.txtwas produced by the first build of the jar. The/liveendpoint used for04-live-set.txtwas added afterwards; it does not touch the/quotepath, but strictly the two files come from two builds of this module.- Timings drift. Treat them as indicative.
Licence
MIT, as the parent repository.