
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.
Up to Java 26, the object header was 96 bits (12 bytes) in size by default – or 128 bits (16 bytes) if Compressed Class Pointers were switched off (for which there is practically no reason). Since Java 27, it is 64 bits (8 bytes) by default – and this article explains how that works.
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’ll learn:
- How does header compression work?
- Why does this not only reduce memory usage but also boost application performance?
- What the new “Self Forwarded Tag” is all about
- How to enable Compact Object Headers – and from which Java version they’re active by default
- How the planned 4-byte headers are supposed to work
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:
Up to Java 26, the object header consisted by default of a 64-bit “Mark Word” and a 32-bit “Class Word”. Mark Word and Class Word are structured as follows:

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).
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.
From Compressed Class Pointer to Compact Object Header
How can we further compress the object header?
First of all, before Java 24, the Mark Word (as you can see above) contained 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 were 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):

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:

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:

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. This block size is a compromise: larger blocks would save even more bits in the pointer, smaller blocks would waste less space between the class structures.
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:

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 0b11. It then finds the original mark word at the new address.
If the copy operation fails (e.g., because the target area is full), the object stays where it is. Up to Java 23, the garbage collector wrote a pointer into the mark word in this case, too – a pointer to the object itself. The identity hash code and object age were not lost: the garbage collector saved the original mark word on a separate stack beforehand and restored it after the garbage collection.
With a compact object header, however, such a self-reference would also overwrite the class pointer – and the garbage collector still needs it, e.g., to determine the size of the object. A compact object header must therefore never be replaced by a self-reference.
Instead, only the new “Self Forwarded Tag” bit is set; all other bits remain in place. Since Java 24, this applies to both header layouts, by the way – the classic 96-bit layout has had a self-forwarded bit since then, too.
Conclusion on Compact Object Headers
Compact Object Headers significantly reduce a Java program’s memory usage by shrinking object headers from 96 bits (12 bytes) to 64 bits (8 bytes). The JEP itself mentions a reduction in heap usage of 10–20 %, Amazon even reports 22 % (the same SPECjbb2015 measurement is also cited in JEP 534: 22 % less heap, 8 % less CPU time), and Alibaba cites values between 5 and 10 %.
And that’s not all – since the objects are smaller, more of them fit into the CPU cache. This leads to fewer cache misses, which has an additional positive effect on performance. Amazon measured 8 to 11 % more throughput, Reddit user “plokhotnyuk” measured 10 % more throughput in a JSON benchmark, and Alibaba reports 6–9 % more throughput.
Enabling Compact Object Headers: -XX:+UseCompactObjectHeaders
Since Java 27, Compact Object Headers are enabled by default via JEP 534; if needed, you can turn them off with -XX:-UseCompactObjectHeaders. In Java 25 and 26, you have to enable them; in Java 24, you additionally have to unlock them as an experimental feature:
| Java version | Status | VM option |
|---|---|---|
| 24 | experimental (JEP 450) | -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders |
| 25, 26 | product feature, off by default (JEP 519) | -XX:+UseCompactObjectHeaders |
| 27 and later | on by default (JEP 534) | disable with -XX:-UseCompactObjectHeaders |
Outlook
According to JEP 534, the old 96-bit layout is to be deprecated in a future Java version.
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” budgets at most 5 % lower throughput or 5 % higher latency for this – and only in rare cases. How the compression to four bytes is to be achieved is not yet detailed in the draft – but it is in the Lilliput wiki.
How Are 4-Byte Headers Supposed to Work?
On the wiki page “Lilliput 2 – 4-byte headers”, Roman Kennke, the author of the JEP draft, outlined the plan in 2024. The 32-bit header is to look like this (H stands for the two hash bits, V for the four bits reserved for Project Valhalla, S again for the Self Forwarded Tag, and T for the two tag bits; I’ll explain the hash bits in a moment):
The following three ideas make the halving possible:
Class pointer reduced to 19 bits. That addresses around 500,000 classes – more than enough for the vast majority of applications. For the rest, a fallback is planned: if the address space is exhausted when a class is loaded, its objects get an additional hidden field holding the full class pointer. The header then contains a reserved bit pattern instead of the compressed class pointer, followed by the offset of that field within the object.
Identity hash code moved out of the header. There is no room left for the 31 bits; only two bits remain in the header to track the state of the hash code:
00– never requested (the normal case for most objects),01–System.identityHashCode()has been called; the hash code is computed from the object’s memory address and stays the same as long as the object is not moved,10– stored in a hidden field at the end of the object.
If the garbage collector has to move an object in state 01, it computes the hash code one last time from the old address, appends the computed hash code to the end of the object’s copy, and sets the copy’s state to 10. If the object’s field layout already has a gap of at least four bytes, it skips state 01: the first call to System.identityHashCode() stores the hash code in that gap and sets the state to 10 – the object then doesn’t have to grow at all. Objects whose hash code is never requested thus keep their size; only the others grow by one field, and only if they have no gap.
GC forwarding with a side table. The problem: when a full GC moves an object, it temporarily records the new address in the old header – as described above. In a 4-byte header, there is hardly any room for that: the class pointer and the hash bits have to stay in place because the garbage collector derives the object size from them. That leaves eleven bits – and no address fits into eleven bits.
The trick: a full GC doesn’t move objects around at random; it slides the live objects, in their existing order and without gaps, to the start of the heap – that is, to lower addresses. If you divide the heap into 4 KB blocks, it is therefore predictable where the objects of a block end up: they stay together and occupy at most 4 KB in total; their starting points – and that is all a forwarding pointer has to address – thus lie in at most two target blocks. Two rather than one, because the sequence usually starts in the middle of a target block – right where the objects of the previous block end – and a later object then already starts in the next target block:
So before it moves anything, the garbage collector records these two target blocks for each source block in a table next to the heap – the side table, which takes up at most 1/256 of the heap size. An object’s new address then fits into eleven bits: one bit says which of the two target blocks is meant, nine bits give the position within that block in 8-byte steps (29 × 8 bytes = 4 KB), and one bit marks the object as moved. If, exceptionally, an object starts in neither of the two blocks, its new address goes into a fallback hash table.
The Parallel GC is still an open issue: its full GC assumes a fixed object size and has to be rewritten for the compact identity hash code. Whether and when all of this will make it into a Java release is open – the JEP draft has not changed since January 2025.
Summary
Compact Object Headers shrink the object header from 96 to 64 bits (12 → 8 bytes) without losing any information: the class pointer is compressed from 32 to 22 bits by addressing the class space in 1 KB blocks instead of byte by byte. The freed-up bits go to Project Valhalla and to the Self Forwarded Tag, with which the garbage collector marks an object it could not copy – without overwriting the class pointer.
This saves 10–20 % of heap (Amazon measures 22 %) and brings a few percent more throughput through fewer cache misses. In Java 24, Compact Object Headers were experimental (JEP 450); in Java 25 and 26, you have to enable them with -XX:+UseCompactObjectHeaders (JEP 519); since Java 27, they are the default (JEP 534).
The next step is 4-byte headers: the class pointer shrinks to 19 bits, the identity hash code leaves the header, and GC forwarding runs through a side table. So far, that is a plan in the Lilliput wiki and a JEP draft without a target version.
If this article has helped you, I would greatly appreciate a positive review on my ProvenExpert profile. Your feedback helps me improve my content and motivates me to write new informative articles.




