Skip to content
Back
JVM internals

JVM Object Headers and Java 25 Compact Headers

Why a Point with two ints takes 24 bytes, what the mark word stores, and how Java 25's compact object headers cut the header from 12 to 8 bytes.

Michał Artur Marciniak

5 min read

On this page
  1. Object Headers - The Hidden Cost
  2. Java 25 - The Latest Chapter
  3. Summary
  4. References

Part 1 followed method calls through the stack and its frames. This part turns to the heap and asks what a single object really costs: its header, its padding, and what Java 25’s compact headers take away.

Object Headers - The Hidden Cost

The Header Tax

Create a simple object:

Java
class Point {
    int x;
    int y;
}

Expected: 8 bytes
Traditional JVM: 24 bytes

Every object carries 12 bytes of header metadata:

  • Mark word8 bytesHash, lock state, GC age
  • Class pointer4 bytesReference to class metadata

Every object is also padded to a multiple of 8 bytes. The fields of Point start right after the header, at offset 12, and 4 bytes of padding go at the end of the object. The combined header plus padding costs 2× the actual data for small objects.

Java 25 changes the math. By embedding a 22-bit class index into unused Mark Word bits, the JVM compresses the header from 12 bytes to 8 bytes:

Traditional24 bytes

Mark wordMark word8 BClass pointerClass4 Bxx4 Byy4 BPaddingPad4 B

Compact (Java 25)16 bytes, 33% less

HeaderHeader8 Bxx4 Byy4 B
A Point with two int fields, to scale. In the traditional layout the fields start at offset 12 and 4 bytes of padding round the object up to a multiple of 8. The compact header takes 8 bytes, so the padding goes away.

Arrays also carry a 4-byte length field, and their size is rounded up to 8 bytes as well, so the saving often disappears: int[10] is 56 bytes in both cases - compact headers mainly benefit small objects without array overhead.

Enable with: -XX:+UseCompactObjectHeaders

Seeing It In Action

Let’s use Java Object Layout (JOL) to inspect actual object sizes:

Java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class ObjectHeaderDemo {
    public static void main(String[] args) {
        System.out.println(VM.current().details());

        // Simple object with two int fields
        class Point {
            int x;
            int y;
        }

        Point p = new Point();
        System.out.println("=== Point Object Layout ===");
        System.out.println(ClassLayout.parseInstance(p).toPrintable());

        // Array header with length field
        int[] intArray = new int[10];
        System.out.println("\n=== int[10] Array Layout ===");
        System.out.println(ClassLayout.parseInstance(intArray).toPrintable());

        // Demonstrate hash code storage
        Object o = new Object();
        System.out.println("\n=== Before hashCode() ===");
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        int hash = o.hashCode();
        System.out.println("\n=== After hashCode(): " + hash + " ===");
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

Expected output analysis:

  • Point object: 12 bytes header + 8 bytes fields + 4 bytes alignment padding at the end = 24 bytes (traditional), 16 bytes (compact)
  • Bare Object: 12 bytes header + 4 bytes padding = 16 bytes (traditional), 8 bytes (compact) - the most dramatic single-object reduction
  • int[10] array: 12 bytes header + 4 bytes length + 40 bytes data = 56 bytes (already 8-byte aligned, no extra padding needed)
  • Hash code: Notice how the Mark Word changes after hashCode() is called - the hash is computed once and cached in the header

The Mark Word: A Multi-Purpose Chameleon

The 64-bit Mark Word is remarkably versatile. It stores:

  • Identity hash code (31 bits): Computed lazily on first hashCode() call
  • GC age (4 bits): Number of GC cycles survived (max 15 before tenuring)
  • Lock state (3 bits): Unlocked, lightweight locked, heavyweight locked, marked for GC

The same 64 bits serve different purposes depending on the object’s state. When you call hashCode(), bits that were unused suddenly store your hash. When you synchronized(obj), the bits transform to hold lock information. During GC, they become forwarding pointers.

Key Takeaway: Object headers (12 bytes) plus alignment padding create 16 bytes of overhead minimum - often consuming 50-67% of memory for small objects. In applications creating millions of small objects (microservices, caches, JSON parsing), this overhead compounds into gigabytes of wasted RAM.


Java 25 - The Latest Chapter

Compact Object Headers (JEP 519)

Java 25 introduces Compact Object Headers as a production-ready feature (experimental in Java 24). The innovation is elegant: compress the 12-byte header down to 8 bytes.

How it works: The JVM overlays the class pointer onto unused bits in the Mark Word. Traditional headers have a separate Mark Word (64 bits) and Class Pointer (32 bits). Compact headers embed a 22-bit class index into the Mark Word itself.

Why 22 bits? Because 2²² = 4,194,304 - enough to address over 4 million unique classes. Even the largest applications rarely exceed this.

Project Lilliput: This feature represents the first deliverable from Project Lilliput, OpenJDK’s initiative to minimize Java object memory overhead. The project roadmap targets 4-byte headers in a future release - halving the size once again. For now, the 12→8 byte reduction is just the beginning.

The Math:

Traditional96 bits = 12 bytes

Mark word64 bitsClass pointer32 bits

Compact (Java 25)64 bits = 8 bytes

Hash, age, lock42 bitsClass pointer22 bits
The compact header is a single 64-bit word: the class pointer, shrunk to 22 bits, moves inside the mark word. A Point with two ints drops from 24 to 16 bytes.

33% reduction (12→8 bytes).

Note that the percentage savings vary by object size: a bare Object with no fields shrinks from 16 bytes (12 header + 4 padding) to 8 bytes - a 50% reduction. Objects with fields see proportionally less dramatic savings as the data payload dominates.

Real-World Impact

JEP 519 reports these measurements, and Compact Object Headers have been tested at Amazon by hundreds of services in production:

  • Heap usage22% lessSPECjbb2015
  • CPU time8% lessSPECjbb2015
  • Garbage collections15% fewerSPECjbb2015, G1 and Parallel
  • JSON parsing10% less timeParallel parser benchmark

The gains are most dramatic for applications with many small objects:

  • Spring Boot microservices (thousands of small objects per request)
  • In-memory caches (Caffeine, Ehcache)
  • JSON/Message processing pipelines

Applications with few, large objects (video processing, image manipulation) see less benefit - the header was already negligible compared to data payload.

Enabling Compact Headers

Just add one JVM flag:

Shell
# Java 24 (experimental  -  unlock needed)
java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders -jar myapp.jar

# Java 25 (production-ready)
java -XX:+UseCompactObjectHeaders -jar myapp.jar

# Verify it's working
java -XX:+UseCompactObjectHeaders -XX:+PrintFlagsFinal 2>&1 | grep UseCompactObjectHeaders

That’s it. No code changes. No recompilation. No regression testing.

Key Takeaway: Java 25 delivers 33% reduction in object header overhead with a single flag. It’s the most memory-efficient Java release ever - without changing a line of application code.


Summary

  1. Object Headers: 12 bytes (Mark Word + Class Pointer), with alignment padding to a multiple of 8 bytes at the end of the object
  2. Java 25: Compact Object Headers reduce overhead from 12 to 8 bytes (33% reduction)

Once you internalize these fundamentals, you start seeing hidden costs everywhere. A cache with a million entries pays 12MB in header overhead before storing a single byte of data. A microservice processing thousands of requests per second allocates more bytes for headers than for the actual request payload.

This overhead isn’t inevitable. Java 25 cuts it by a third with a single flag - no code changes, no recompilation, no testing cycles. The JVM spent thirty years optimizing garbage collection down to millisecond pauses. Now it’s optimizing the objects themselves.

Add -XX:+UseCompactObjectHeaders and measure the difference.


References

  1. JEP 519: Compact Object Headers

  2. Java Object Headers Deep Dive

  3. Java 25 Compact Headers Integration

  4. Understanding Object Headers

  5. Java Object Layout (JOL)