JEP 450: Compact Object Headers (Experimental)

OwnerRoman Kennke
TypeFeature
ScopeImplementation
StatusIntegrated
Release24
Componenthotspot / runtime
Discussionhotspot dash dev at openjdk dot org
EffortL
DurationL
Reviewed byAleksey Shipilev, Erik Österlund, John Rose, Stefan Karlsson, Thomas Stuefe
Endorsed byVladimir Kozlov
Created2022/10/07 19:27
Updated2024/11/12 19:06
Issue8294992

Summary

Reduce the size of object headers in the HotSpot JVM from between 96 and 128 bits down to 64 bits on 64-bit architectures. This will reduce heap size, improve deployment density, and increase data locality.

Goals

When enabled, this feature

When disabled, this feature

This experimental feature will have a broad impact on real-world applications. The code might have inefficiencies, bugs, and unanticipated non-bug behaviors. This feature must therefore be disabled by default and enabled only by explicit user request. We intend to enable it by default in later releases and eventually remove the code for legacy object headers altogether.

Non-Goals

It is not a goal to

Motivation

An object stored in the heap has metadata, which the HotSpot JVM stores in the object's header. The size of the header is constant; it is independent of object type, array shape, and content. In the 64-bit HotSpot JVM, object headers occupy between 96 bits (12 bytes) and 128 bits (16 bytes), depending on how the JVM is configured.

Objects in Java programs tend to be small. Experiments conducted as part of Project Lilliput show that many workloads have average object sizes of 256 to 512 bits (32 to 64 bytes). This implies that more than 20% of live data can be taken by object headers alone. Thus even a small improvement in object header size could yield a significant reduction in footprint, data locality, and reduced GC pressure. Early adopters of Project Lilliput who have tried it with real-world applications confirm that live data is typically reduced by 10%–20%.

Description

Compact object headers is an experimental feature and therefore disabled by default. Compact object headers can be enabled with -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders.

Current object headers

In the HotSpot JVM, object headers support many different features:

The current object header layout is split into a mark word and a class word. The mark word comes first, has the size of a machine address, and contains:

Mark Word (normal):
 64                     39                              8    3  0
  [.......................HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT]
         (Unused)                      (Hash Code)     (GC Age)(Tag)

In some situations, the mark word is overwritten with a tagged pointer to a separate data structure:

Mark Word (overwritten):
 64                                                           2 0
  [ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppTT]
                            (Native Pointer)                   (Tag)

When this is done, the tag bits describe the type of pointer stored in the header. If necessary, the original mark word is preserved (displaced) in the data structure to which this pointer refers, and the fields of the original header, i.e., the hash code and age bits, are accessed by dereferencing the pointer to get to the displaced header.

The class word comes after the mark word. It takes one of two shapes, depending on whether compressed class pointers are enabled:

Class Word (uncompressed):
64                                                               0
 [cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc]
                          (Class Pointer)

Class Word (compressed):
32                               0
 [CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC]
     (Compressed Class Pointer)

The class word is never overwritten, which means that an object's type information is always available, so no additional steps are required to check a type or invoke a method. Most importantly, the parts of the runtime that need that type information do not have to cooperate with the locking, hashing, and GC subsystems, which can change the mark word.

Compact object headers

For compact object headers, we remove the division between the mark and class words by subsuming the class pointer, in compressed form, into the mark word:

Header (compact):
64                    42                             11   7   3  0
 [CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT]
 (Compressed Class Pointer)       (Hash Code)         /(GC Age)^(Tag)
                              (Valhalla-reserved bits) (Self Forwarded Tag)

Locking operations no longer overwrite the mark word with a tagged pointer, thus preserving the compressed class pointer. GC forwarding operations become more complex in order to preserve direct access to the compressed class pointer, requiring a new tag bit, as discussed below. The size of the hash code does not change. We reserve four bits for future use by Project Valhalla.

Compressed class pointers

Today's compressed class pointers encode a 64-bit pointer into 32 bits. They are enabled by default, but can be disabled via -XX:-UseCompressedClassPointers. The only reason to disable them, however, would be for an application that loads more than about four million classes; we have yet to see such an application.

Compact object headers require compressed class pointers to be enabled and, moreover, reduce the size of compressed class pointers from 32 bits to 22 bits by changing the compressed class pointer encoding.

Locking

The HotSpot JVM's object-locking subsystem has two levels.

HotSpot also supports the legacy stack-locking mechanism. This spiritual predecessor to lightweight locking associates the locked object with the locking thread by copying the object header to the thread's stack and overwriting the object header with the pointer to the header copy. This is problematic for compact object headers because it overwrites the object header and thus loses crucial type information. Therefore, compact object headers are not compatible with legacy locking. If the JVM is configured to run with both legacy locking and compact object headers then compact object headers are disabled.

GC forwarding

Garbage collectors that relocate objects do so in two steps: First they copy an object and record the mapping between its old and new copies (i.e., forwarding), then they use this mapping to update references to the old copy in either the entire heap or just a particular generation.

Of the current HotSpot GCs, only ZGC uses a separate forwarding table to record forwardings. All other the GCs record forwarding information by overwriting the header of the old copy with the location of the new copy. There are two distinct scenarios that involve headers.

GC walking

Garbage collectors frequently walk the heap by scanning objects linearly. This requires determining the size of each object, which requires access to each object's class pointer.

When the class pointer is encoded in the header, some simple arithmetic is required to decode it. The cost of doing this is low compared to the cost of the memory accesses involved in a GC walk. No additional implementation work is needed here since the GCs already access class pointers via a common VM interface.

Alternatives

Testing

Changing the header layout of Java heap objects touches many HotSpot JVM subsystems: the runtime, all garbage collectors, all just-in-time compilers, the interpreters, the serviceability agent, and the architecture-specific code for all supported platforms. Such massive changes warrant massive testing.

Compact object headers will be tested by:

All of these tests will be executed with the feature turned on and off, with multiple combinations of GCs and JIT compilers, and on several hardware targets.

We will also deliver a new set of tests that measure the size of various objects, e.g., plain objects, primitive type arrays, reference arrays, and their headers.

The ultimate test for performance and correctness will be real-world workloads once this experimental feature is delivered.

Risks and Assumptions