JEP 450 (Compact Object Headers) has been targeted for delivery in JDK 24, and has already been merged to main.
This currently experimental feature optimizes heap utilization by shrinking the size of the mandatory object header in HotSpot. This should reduce overall heap size, improve density of deployments, and increase data locality.
Overview of current implementation
HotSpot stores all objects in the Java heap, a contiguous area of the "C heap" of the process. Objects in Java are always handled by reference and so, for example:
- Local variables that refer to objects contain pointers from the stack frame of the Java method into the Java heap.
- Object fields of reference type point from one Java heap location to another.
The target address of a Java reference is always the start of an object header (which is mandatory in current versions of HotSpot).
The header is present on every object (and arrays have an additional 32 bits of header to store the array's length). The mark word is the first 64 bits and is used for instance-specific metadata, i.e. supporting features like:
- Garbage Collection - Storing an object's age (& possibly forwarding pointers)
- Hash codes - Storing an object's stable identity hash code
- Locks - Storing an object's lock / monitor
Under some circumstances, the mark word will be overwritten and replaced by a pointer that refers to a more complex data structure. This slightly complicates the implementation of compact object headers.
After the mark word follows the class (or klass) word, which is used to calculate a pointer to metadata shared by every object of this class type. This is used for method invocation, reflection, type checks, etc.
The klass metadata (or klass) is held in metaspace, which is outside the Java heap, but not outside the C heap of the JVM process. As they exist outside the Java heap, the klasses do not require a Java object header, and they are not the same thing as the class objects used in reflection (which are genuine Java objects).
The klass word was originally a full machine word of header, but this is wasteful on 64-bit architectures, and so a technique called "compressed class pointers" was introduced. This encodes the class pointer into 32 bits (by using a scaling and offsetting method) which works for any application that loads less than about four GB of class files.
So, apart from edge cases, non-array objects on a 64-bit version of HotSpot are paying a "header tax" of 96 bits. This is lightweight, by way of comparison: until quite recently, Python's header tax was 308 bytes, but the purpose of JEP 450 is to do even better, and reduce the total size of the header to 64 bits.
Introducing Compact Object Headers
The new implementation, which has been developed as part of OpenJDK's "Project Lilliput" reduces the object header size on two target 64-bit platforms (x64 and AArch64).
The overall aims are to:
- Cap throughput and latency overhead at 5% on the target platforms, and only approach that limit in infrequent cases
- Not introduce measurable throughput or latency overheads on non-target platforms
In fact, testing currently shows only a very few regressions (with several fixes for them underway for JDK 24). Testing by Amazon so far shows that many workloads actually benefit in throughput, sometimes dramatically - with some workloads seeing drops in CPU utilization of up to 30%.
The project seeks to exploit the observed fact that many Java workloads have small average object sizes of 32 to 64 bytes. This equates to a ~20% header tax. Thus even a small improvement in object header size could yield a significant reduction in heap footprint. In turn, this could improve data locality and reduce GC pressure, which has further potential performance benefits.
To achieve this header reduction, the mark and class words are combined into a single 64-bit word, laid out like this:
There are several aspects of this that we should note:
- There are now 22 bits (instead of 32) used to identify an objects class type. This means that the number of different class types we can load into a JVM process is around ~4 million.
- The size of the hash code does not change.
- Locking operations no longer overwrite the mark word. This preserves the compressed class pointer.
- GC forwarding operations become more complex in order to preserve direct access to the compressed class pointer.
- There are 4 unused bits that are reserved for future enhancements (e.g. Project Valhalla)
If a Java lock is contended, then the new implementation needs to look up the address of an auxiliary data structure that holds the lock information. This approach, called "object monitor tables" was implemented in JDK 22 and is activated by a new switch UseObjectMonitorTable
that is on by default. Compact object headers relies upon this mechanism.
If no showstoppers are found, this feature will ship (initially as an experimental feature) as part of JDK 24, expected during March 2025. The long-term intent is for this mechanism to become the only header representation on supported platforms, but this is likely to take several more releases. It also depends upon extensive testing on real workloads and a lack of performance and other regressions. There is even ongoing exploratory work to see if reducing header size to 32 bits would be possible.
Once available in JDK 24 (betas or final), application teams can help by testing their workloads with this new feature activated via the command-line switch -XX:UseCompactObjectHeaders
and looking for performance differences associated with it.