Compact Object Headers Feature ImageCompact Object Headers Feature Image
HappyCoders Glasses

Java Compact Object Headers
(JEP 519)

Sven Woltmann
Sven Woltmann
Last update: July 21, 2025

Every Java object has an object header that precedes the actual data in memory. The header mainly contains the hash code of the object and the information on which class the object is an instance of.

As of Java 25, the object header is by default 96 bits (12 bytes) in size – or 128 bits (16 bytes) if Compressed Class Pointers are switched off (although there is no reason to do so, and this option has been deprecated in Java 25).

As part of Project Lilliput, the JDK developers have been working for many years on ways to compress the header to a total of 64 bits or even 32 bits.

In 2025, the time had come: In Java 24, Compact Object Headers were introduced as an experimental feature through JDK Enhancement Proposal 450 – and in Java 25 as a final feature through JDK Enhancement Proposal 519. Compact Object Headers make it possible to compress the object header from 96 bits to 64 bits and thus significantly reduce the heap size of existing applications.

In this article you will find out:

  • How does header compression work?
  • Why does this not only reduce memory requirements but also increase application performance?

Status Quo before Compact Object Headers

You can find a detailed description of the structure of object headers in the main article on Java object headers. Here is a summary of the most essential points:

The object header usually consists of a 64-bit “Mark Word” and a 32-bit “Class Word”. Mark Word and Class Word are structured as follows:

JEP 450: Java Object Header with Mark Word and Class Word

The Mark Word contains:

  • a 31-bit identity hash code (which is returned by the System.identityHashCode(Object) method),
  • 4 bits in which the garbage collector stores the age of an object (based on which it decides when to move an object from the young to the old generation),
  • 2 “tag bits” that indicate whether the object is not locked, locked uncontended (without waiting threads), or locked contended (with waiting threads).

When using the outdated Legacy Stack Locking, the first 62 bits of the mark word in the locked state were replaced by a pointer to a lock data structure. Since Java 23, so-called Lightweight Locking has replaced this outdated mechanism.

The old mode can still be reactivated using the VM option -XX:LockingMode=1; however, it cannot be combined with Compact Object Headers.

The Class Word contains a 32-bit offset in the maximum 4 GB compressed class space to a so-called class data structure containing all relevant data about the object’s class.

If Compressed Class Pointers have been deactivated with -XX:-UseCompressedClassPointers, then the Class Word is 64 bits in size and contains an uncompressed pointer. The deactivation of Compressed Class Pointers has been deprecated in Java 25 and cannot be combined with Compact Object Headers.

From Compressed Class Pointer to Compact Object Header

How can we further compress the object header?

First of all, the Mark Word (as you can see above) currently contains 27 unused bits (25 at the beginning and one each before and after the “age bits”). That means that of the 96 bits of the entire object header, only 96 - 27 = 69 bits are required. To get to 64 bits, we must somehow save five bits.

Where can we get them?

The JDK developers experimented for a long time until they came up with the following solution (I changed the scale for better visualization – the 64 bits now cover the entire width):

JEP 450: Compact Object Header

The new 64-bit header is no longer divided into Mark Word and Class Word but contains the following information directly:

  • a class pointer further compressed from 32 bits to 22 bits (explained below),
  • the 31-bit Identity Hash Code (unchanged),
  • 4 bits reserved for Project Valhalla (new),
  • 4 bits for the age of the object (unchanged),
  • 1 bit for the so-called “Self Forwarded Tag” (explained below),
  • 2 Tag bits (unchanged).

The class pointer has, therefore, been reduced by 10 bits. As we only had to save five bits, five additional bits are now available. Four of these were reserved for Project Valhalla, and the new “Self Forwarded Tag” is stored in one bit.

How Could Class Pointers Be Compressed to 22 Bits?

With the previous 32 bits, we could individually address each position within the 4 GB compressed class space.

For the sake of simplicity, I will illustrate this in the following image with a 256-byte memory area:

256 byte memory area

As you can see, we need the numbers 0 to 255 to address each position of the memory area. To do this, we need an 8-bit pointer (28 = 256).

But do we really need to be able to address every single position? No, we don't!

Just as a hard disk (whether a conventional one or an SSD) is divided into so-called blocks (usually 4 KB in size), we can also divide the memory area for the class data into blocks. This means we no longer have to address each individual byte but only each block. And so we can address the same memory area with significantly fewer bits.

Here again is the simplified example, in which I divided the 256-byte memory area into 32 blocks of 8 bytes each:

256 byte memory area divided into 8 byte blocks

Now, we only need the numbers 0 to 31 to address the same memory area. Therefore, we only need 5-bit pointers (25 = 32). By dividing into blocks, we reduced the memory requirement per pointer from 8 bits to 5 bits.

This also works with the memory area in which the class information is stored.

When using Compact Object Headers, this memory area is divided into 1,024 (=210) byte blocks. The JDK developers chose this value because most classes occupy between half a kilobyte and one kilobyte.

As a reminder, the area is 4 GB in size. This results in 4 × 1,024 × 1,024 × 1,024 / 1,024 blocks, i.e. 4 × 1,024 × 1,024, which is 4,194,304, or 222 blocks. And we can address these with 22 bits!

To turn a 22-bit block number into a pointer, we only have to shift the 22 bits to the left by 10 bits and fill the last 10 bits with zeros, and we have a 32-bit pointer into the 4 GB memory area again:

22-bit block number becomes a 32-bit class pointer

The division into blocks now leads to class data fragmentation. However, the JDK developers have also considered this: the memory between the classes can also be used by other data structures in the metaspace.

What Is the “Self Forwarded Tag”?

When a garbage collector copies an object to a new memory address, it replaces the upper 62 bits of the mark word in the original object with a pointer to the new memory address and sets the tag bits to 0x11. It then finds the original mark word at the new address.

If the copy operation fails, the mark word is replaced by a pointer to the object itself. As a result, the identity hash code and object age are lost, but this seems to be bearable (unfortunately, I could not find any reliable information about why this is the case, but I will update this section if I find a statement on this).

However, if we were to replace a compact object header with a self-reference, the class pointer would also be lost. As this pointer is essential, a compact object header must never be replaced by such a self-reference.

Instead, the new “Self Forwarded Tag” bit is set.

Conclusion on Compact Object Headers

Compact Object Headers significantly reduce the memory requirements of a Java program by reducing the object headers from 96 bits (12 bytes) to 64 bits (8 bytes).

Not only that: because the objects are smaller, more objects fit into the CPU cache. This results in fewer cache misses – and this also has a positive effect on performance.

As of Java 25, Compact Object Headers can be activated with the following VM option:

-XX:+UseCompactObjectHeaders

In Java 24, Compact Object Headers were still in the experimental stage and had to be activated as follows:

-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders

Outlook

In an upcoming Java release (the exact version is not yet known), Compact Object Headers will be enabled by default (see JEP draft “Compact Object Headers by Default”).

Next, the Project Lilliput developers are working on 4-byte headers – effectively halving the header size once again! This further reduction will likely come at the cost of some performance. The JEP draft “4-byte Object Headers” targets a maximum throughput and latency reduction of 5%. How the compression to four bytes is to be achieved has not yet been detailed in the draft.

Have you already tested Compact Object Headers? Did it bring the expected improvements? Share your experience in the comments!