Versions tested. Temurin 25.0.4.1+1 (LTS) and Temurin 27+35 (GA 2026-09-15), Spring Boot 4.1.1. Machine: 2 vCPU, 8 GB RAM, with the load generator on the same machine, so absolute numbers are modest and noisy. Three repetitions per configuration, interleaved. Companion code and raw output: jdk27-perf in the javademos repository.
The two defaults that changed
The OpenJDK page for JDK 27 lists the features in the release. Two of them affect a server without any code change. JEP 534 turns on compact object headers by default. Every Java object starts with a header; the compact form shrinks it from 12 bytes to 8, so small objects take less memory. JEP 523 makes G1 the default collector in all environments. On JDK 25 the JVM picked the serial collector instead when a container looked too small (fewer than about two CPUs or less than about 1.8 GB), which is why the same app could behave differently in a tiny pod. You can see the first change directly. This is from 01-defaults.txt, the JVM’s own report of its defaults on this machine:== JDK 27
openjdk version "27" 2026-09-15
size_t MaxHeapSize = 1568669696 {product} {ergonomic}
bool UseCompactObjectHeaders = true {product lp64_product} {default}
bool UseG1GC = true {product} {ergonomic}
bool UseSerialGC = false {product} {default}
On JDK 25 the same file shows UseCompactObjectHeaders = false. Both JDKs chose G1 on this 2-CPU, 6 GB-visible machine, so the collector is identical in every comparison below. That is deliberate: it isolates headers and the rest of the JDK from the GC choice. (The container effect of JEP 523 is covered in JDK 27 memory changes.)
The service under test
A benchmark of an empty app would measure nothing, and a benchmark of a toy loop would not look like a server. This service has two properties of a real one. It holds a large resident data set of small objects: 3,000,000 records of two ints each, so object headers matter. And its request path allocates and serialises:/quote?n=200 builds 200 response records and Jackson turns them into JSON, so the GC and the JIT matter. The source is PerfApplication.java, and the closed-loop load generator is Load.java (4 threads, 15 seconds of warmup discarded, 30 seconds measured).
I ran four configurations: JDK 25 as shipped, JDK 25 with compact headers switched on, JDK 27 as shipped, and JDK 27 with compact headers switched off. The two extra rows are the useful ones: they separate “what the new default does” from “what else changed in the JDK”.
Result one: the resident data set shrank by about 24 MiB
To measure memory fairly I did not read the heap during load, because the used heap includes garbage that has not been collected yet and changes with every GC. Instead the harness forces a full collection and then reads what is still in use. That is the resident data set. These are the first four lines of 04-live-set.txt (the three repetitions agreed to within 1 MiB):jdk25 defaults: live heap after full GC 93 MiB
jdk25 +CompactObjectHeaders: live heap after full GC 69 MiB
jdk27 defaults: live heap after full GC 70 MiB
jdk27 -CompactObjectHeaders: live heap after full GC 93 MiB
Result two: speed did not change in any way I can measure
Now the part a release announcement would prefer to skip. These are the medians of three runs from 03-summary.txt:jdk25 +CompactObjectHeaders | startup 3512 ms | throughput 7157 req/s | p99 3.09 ms | heap used 114 MiB | RSS 361 MiB
jdk25 defaults | startup 3437 ms | throughput 6788 req/s | p99 3.23 ms | heap used 162 MiB | RSS 403 MiB
jdk27 -CompactObjectHeaders | startup 3388 ms | throughput 7010 req/s | p99 3.03 ms | heap used 189 MiB | RSS 458 MiB
jdk27 defaults | startup 3220 ms | throughput 7016 req/s | p99 2.99 ms | heap used 128 MiB | RSS 432 MiB
Reading medians alone would be misleading, so here are the throughput ranges across the three runs of each configuration:
| Config | Median req/s | Lowest–highest run | p99 (median) |
|---|---|---|---|
| JDK 25 defaults | 6788 | 6239–6868 | 3.23 ms |
| JDK 25 +compact headers | 7157 | 6407–7228 | 3.09 ms |
| JDK 27 defaults | 7016 | 6755–7275 | 2.99 ms |
| JDK 27 −compact headers | 7010 | 6867–7227 | 3.03 ms |
Going deeper: why RSS and “heap used” are not a memory result
The summary lines above include
heap used and RSS columns, and they look like a memory comparison. They are not a reliable one. Heap used is read at one instant after load and includes garbage; RSS includes everything G1 decided to keep committed. Here are the per-run values from 02-runs.txt, resident set size in MiB:
| Config | RSS across 3 runs |
|---|---|
| JDK 25 defaults | 325, 403, 415 |
| JDK 25 +compact headers | 313, 361, 363 |
| JDK 27 defaults | 386, 432, 434 |
| JDK 27 −compact headers | 454, 458, 475 |
What to do with this
You do not need to leave JDK 25 to get the memory win. The row “jdk25 +CompactObjectHeaders” ran with just -XX:+UseCompactObjectHeaders on Temurin 25.0.4.1 and produced the same 69 MiB live heap as JDK 27’s default. If you run the LTS and keep many small objects in memory, try that flag, compare your own live set, and keep it if the numbers hold. Test it under your real workload first; I measured one service on one machine.
Should you move to JDK 27 for speed? Not on this evidence. JDK 27 is a feature release, not an LTS, and what I measured was a footprint improvement you can also get on 25 with one flag. Move because you want the features or have a short upgrade cycle, not because of a throughput number.
Reproduce it, and the caveats on method
Build with
mvn -B -DskipTests package, set J25 and J27, and run run-all.sh (about 12 minutes). Use REPS=5 for tighter medians. Configurations are interleaved within each repetition so any slow drift on the machine hits all four equally.
Two caveats from the module README. First, the throughput file was produced by the first build of the jar; the /live endpoint used for the live-heap measurement was added afterwards. It does not touch the /quote request path, but strictly the two result files come from two builds. Second, timings drift between runs and machines; treat them as indicative.
No Comments yet!