Compact Object Headers (JEP 519): 33% Less Heap Overhead
What Is an Object Header?
Every single Java object — every String, every Integer, every record, every array — carries a header that the JVM uses for bookkeeping. Your code never sees this header; it lives alongside the object’s fields in memory.
Before Java 25, on a 64-bit JVM, the header occupied 96 to 128 bits (12–16 bytes):
┌─────────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
│ ─ identity hash code │
│ ─ lock state (biased lock / thin lock / fat lock) │
│ ─ GC age bits │
├─────────────────────────────────────────────────────────┤
│ Class Pointer (32 bits compressed / 64 bits full) │
│ ─ pointer to the object's class (Klass* in HotSpot) │
└─────────────────────────────────────────────────────────┘
With UseCompressedOops (default), the class pointer is compressed to 32 bits, giving a 96-bit (12-byte) header. Without compression (large heaps > 32GB), it’s 128 bits (16 bytes).
Why This Matters
Twelve bytes per object doesn’t sound like much. But consider:
- A
HashMapwith 1,000,000 entries creates ~3–4 million objects (the map itself, eachEntrynode, each key, each value) - A typical microservice processes thousands of requests per second, each allocating hundreds of short-lived objects
- A data pipeline processing 10 million records allocates 10M+ wrapper objects
For a workload with 100 million live objects, 12 bytes per header = 1.2 GB of header overhead alone. That’s memory your application’s actual data is competing with for heap space.
Java 25: 64-Bit Compact Headers
JEP 519 packs the same information into 64 bits by redesigning the encoding:
┌─────────────────────────────────────────────────────────┐
│ Compact Header (64 bits total) │
│ [0–31] compressed class pointer (32 bits) │
│ [32–54] identity hash code (22 bits — still enough) │
│ [55–58] GC age (4 bits) │
│ [59–62] lock state (4 bits) │
│ [63] reserved (1 bit) │
└─────────────────────────────────────────────────────────┘
Header size drops from 96 bits → 64 bits — a 33% reduction.
How to Enable It
Compact Object Headers are on by default in Java 25 for most workloads. You can verify or force it with a JVM flag:
# Enable explicitly (already the default in Java 25)
java -XX:+UseCompactObjectHeaders -jar myapp.jar
# Disable if you hit compatibility issues
java -XX:-UseCompactObjectHeaders -jar myapp.jar
No code changes required. This is a pure JVM-level optimization.
What Performance Gains to Expect
The gains are workload-dependent:
| Workload type | Expected improvement |
|---|---|
| Object-heavy (collections, graphs) | 10–20% heap reduction |
| String-heavy (parsers, serializers) | 8–15% heap reduction |
| Compute-heavy (math, no allocations) | Negligible |
| GC pressure reduction | Reduced GC frequency (less heap = longer between GCs) |
| Cache efficiency | Better — objects fit more tightly in CPU cache lines |
From the JEP benchmarks on the SPECjbb2015 suite: ~10% throughput improvement on heap-bound workloads.
Demonstrating with a Simple Benchmark
You can measure the impact with a quick allocation benchmark:
import module java.base;
public class HeaderBenchmark {
record Point(int x, int y) {}
public static void main(String[] args) {
int count = 10_000_000;
long before = Runtime.getRuntime().totalMemory()
- Runtime.getRuntime().freeMemory();
Point[] points = new Point[count];
for (int i = 0; i < count; i++) {
points[i] = new Point(i, i * 2);
}
System.gc();
long after = Runtime.getRuntime().totalMemory()
- Runtime.getRuntime().freeMemory();
System.out.printf("Allocated %,d Points%n", count);
System.out.printf("Heap delta: %,.1f MB%n", (after - before) / 1_048_576.0);
System.out.printf("Bytes per Point (approx): %.1f%n",
(double)(after - before) / count);
// Prevent GC from collecting points before measurement
if (points[0] == null) System.out.println("unreachable");
}
}
Run with and without compact headers:
# With compact headers (Java 25 default)
java -XX:+UseCompactObjectHeaders HeaderBenchmark.java
# Without compact headers
java -XX:-UseCompactObjectHeaders HeaderBenchmark.java
A Point record has two int fields (8 bytes of data). With compact headers:
- Java 24: 8 bytes data + 12 bytes header = 20 bytes → padded to 24 bytes
- Java 25: 8 bytes data + 8 bytes header = 16 bytes → 16 bytes (no padding needed)
For 10M Points: 24 × 10M = 240 MB vs. 16 × 10M = 160 MB — 80 MB saved, a 33% reduction exactly.
Impact on Arrays
Arrays also benefit because each array element is an object reference or a primitive value — but array objects themselves have headers:
// An int[] with 1000 elements
// Java 24: 12-byte header + 4 bytes length + (1000 × 4 bytes) = 4016 bytes → padded to 4016
// Java 25: 8-byte header + 4 bytes length + (1000 × 4 bytes) = 4012 bytes → padded to 4016
For arrays, the impact is smaller because the header overhead is amortized over many elements. The biggest wins come from small objects where the header is a large fraction of total size.
Impact on Records and Small Value Types
Records — one of the best features of modern Java — are especially impacted. A typical record:
record Coordinate(double lat, double lon) {}
- Field size: 8 + 8 = 16 bytes
- Java 24 header: 12 bytes → total 28 bytes → padded to 32 bytes
- Java 25 header: 8 bytes → total 24 bytes → padded to 24 bytes
For an application tracking a million GPS coordinates: 32 MB → 24 MB. Every cache line holds 3 coordinates instead of 2 — better spatial locality, fewer cache misses.
Compatibility Notes
Compact Object Headers required several internal changes to HotSpot:
Identity hash code: The hash code now uses 22 bits instead of 31 bits. For
Object.hashCode()this is still sufficient for most use cases (4 million possible values per object).JVM TI agents: Native agents that read raw object headers need updating. Most production agents (profilers, APM tools) updated for Java 25.
Off-heap access (Unsafe): Code using
sun.misc.Unsafeto read object headers directly is fragile regardless — Java 25 doesn’t change this, but compact headers change the offset. Use the officialVarHandleAPI instead.
If you have a native agent that’s incompatible, disable with -XX:-UseCompactObjectHeaders while you wait for an update.
Summary
| Java 24 | Java 25 | |
|---|---|---|
| Header size | 96 bits (12 bytes) | 64 bits (8 bytes) |
| Code changes required | N/A | None — JVM flag |
| Enabled by default | No | Yes |
| Heap saving | — | ~10–20% on object-heavy workloads |
| Identity hash bits | 31 | 22 |
Compact Object Headers is one of the most impactful Java 25 improvements for production systems because it requires zero code changes and delivers real memory savings on any object-heavy workload.
Next up: Generational Shenandoah →