Java 27 Compact Object Headers: Benchmarked
Java 27 went GA today — Mark Reinhold shipped build 35 with nine JEPs, and the one with teeth for anyone running fleets of JVMs is JEP 534: Compact Object Headers by default. JEP 450's research claimed 10-20% footprint reductions. I downloaded JDK 27 build 35 and JDK 25 build 36 and measured it myself. Result: 29.21 → 21.11 bytes per object — a 27.7% heap reduction for objects in the sweet spot, and exactly zero savings for objects where 8-byte alignment swallows the gain. That second number is the part the press release won't tell you.
Setup: two JDKs, one box, no excuses
Both builds straight from Oracle's GPL distribution, same 2-core / 1 GB Linux VM:
$ curl -sL -o jdk27.tar.gz https://download.java.net/java/GA/jdk27/.../35/GPL/openjdk-27_linux-x64_bin.tar.gz
$ ./jdk-27/bin/java -version
openjdk version "27" 2026-09-15
OpenJDK Runtime Environment (build 27+35-2325)
$ ./jdk-25/bin/java -version
openjdk version "25" 2025-09-16
Flag check first — is the feature actually where the JEP says it is?
$ jdk-27/bin/java -XX:+PrintFlagsFinal -version | grep CompactObjectHeaders
bool UseCompactObjectHeaders = true {product lp64_product} {default}
$ jdk-25/bin/java -XX:+PrintFlagsFinal -version | grep CompactObjectHeaders
bool UseCompactObjectHeaders = false {product lp64_product} {default}
Confirmed: identical VM, one flag flipped between releases. The benchmark is Java 12/27's compact header state, not G1 differences or JIT noise.
Method
Keep a known count of small objects live, force three full GCs, read the used heap:
static class Q { int a; Q next; } // 8 bytes of fields
long target = 4_000_000;
ArrayList<Q> keep = new ArrayList<>(4_000_000);
for (int i = 0; i < target; i++) { ... keep.add(head); }
for (int i = 0; i < 3; i++) { System.gc(); Thread.sleep(300); }
long used = rt.totalMemory() - rt.freeMemory();
Every configuration runs three times. Used heap is stable to within ±0.1% across runs, so the numbers below are real signal, not noise.
Results: the sweet spot is real — and narrow
| Configuration | bytes/object (4M objects, 8B fields) | Heap used |
|---|---|---|
| JDK 25 default (COH off) | 29.21 | 116.8 MB |
JDK 25 + -XX:+UseCompactObjectHeaders | 21.13 | 84.5 MB |
| JDK 27 default (COH on) | 21.11 | 84.4 MB |
JDK 27 + -XX:-UseCompactObjectHeaders | 29.17 | 116.7 MB |
Exactly 8 bytes saved per object, 27.7% off the heap. JDK 27's default is bit-for-bit equivalent to explicitly opting in on 25 — no hidden partial rollout.
The alignment trap: where the savings are zero
My first two benchmarks were flat, and that failure is the most useful finding here. I built test objects with 12 bytes of fields (two ints + one reference). Result: 36.0 vs 36.0 bytes per object — identical across every flag combination.
The reason is arithmetic. Legacy object headers are 12 bytes (8-byte mark word + 4-byte compressed class pointer); compact headers are 8. But objects are padded to 8-byte boundaries:
graph LR
A["12B fields"] --> B{"legacy: align8(12+12)=24"}
A --> C{"compact: align8(8+12)=24"}
B --> D["NO SAVINGS"]
C --> D
E["8B fields"] --> F{"legacy: align8(12+8)=24"}
E --> G{"compact: align8(8+8)=16"}
F --> H["8 bytes saved"]
G --> H
align8(12+f) and align8(8+f) only diverge when the field size crosses a padding boundary differently — which happens for f ≡ 0 (mod 8) with f > 0. Int-only payload classes, node classes with a single reference, boxed pairs: those win big. Objects with 12-byte or 20-byte field sets: nothing. Your actual workload sits somewhere between those poles, which is why JEP 450's own claims average 10-20% and not 28%.
The default flip in JEP 534 is free money for small-object-heavy workloads and invisible for fat domain objects. Measure before you celebrate.
The rest of the release, in ninety seconds
- JEP 523 — G1 default in all environments. Already ergonomic on this 1 GB box:
UseG1GC = trueeven at-Xmx64m. The JEP formalizes what ergonomics was already doing. - JEP 527 — Post-quantum hybrid key exchange for TLS 1.3. Ships ML-KEM in the default TLS stack. Quietly the most security-relevant JEP of the nine.
- JEP 536 — JFR in-process data redaction. Flight Recorder can now scrub sensitive strings at the source instead of hoping nobody ships heap dumps to a vendor.
- JEP 531 (Lazy Constants), 533 (Structured Concurrency, 7th preview), 538 (PEM encodings) — platform hygiene; nothing changes your architecture today.
Bottom line
Java 27's compact object headers are not vapor: on the right object shapes, I measured a 27.7% heap reduction with zero code changes and zero flag changes after upgrade — just install 27 and run. But the gain is alignment-dependent, and for common 12-byte-field object shapes it is literally zero. If you run memory-billed containers (and who doesn't), this release is worth taking. If you benchmark after upgrading and see nothing, check your object field sizes before you blame the JVM.