Mastering Memory Efficiency with Compact Object Headers in JDK 25

Wanderson Xesquevixos

Compact Object Headers are introduced in JDK 25 through JEP 519, marking a significant step forward for memory optimization in the JVM. First introduced experimentally in JEP 450 with JDK 24, this feature, authored by Roman Kennke, reduces the per-object memory footprint without slowing down performance. 

The payoff is especially clear in object-heavy workloads and memory-constrained environments, such as those found in containers and cloud-native deployments.

In JDK 25, Compact Object Headers graduate from experimental to standard. However, they’re not enabled by default. Developers must turn them on using JVM flags to see the benefits.

This article explains what object headers are, how they change in JDK 25 with Compact Object Headers enabled,  and shows a benchmark comparing applications with and without Compact Object Headers enabled. Let’s start by understanding the traditional object header.

Understanding the Traditional Object Header

In the Java Virtual Machine (JVM), every object in memory begins with an object header, a small block of metadata stored before the object’s actual fields. This header acts as the object’s identity card, containing information the JVM needs for essential operations such as synchronization, garbage collection, and type resolution.

On a JVM, a traditional object header typically consists of two main parts:

  • Mark Word: A compact field used to store runtime metadata. It may hold the object’s identity hash code, garbage collection (GC) age, or locking information such as biased, lightweight, or heavyweight lock state. Because the Mark Word is heavily overloaded, its meaning changes depending on the object’s lifecycle state. 
    On 32-bit systems, it usually takes 4 bytes, and on 64-bit systems, 8 bytes. Under certain operations, the Mark Word can be repurposed as a tagged pointer, for instance, to a lock record during synchronization or a forwarding pointer during GC compaction.
  • Class Pointer (Klass Pointer): A reference to the class metadata that defines the object’s type and field layout. When compressed class pointers (compressed klasses) are enabled, this pointer uses 4 bytes; otherwise, it requires 8 bytes. This compression mechanism is separate from Compact Object Headers: compressed pointers shrink pointer size, while compact headers redesign the header structure itself. On 32-bit JVMs, class pointers are always 4 bytes in size, so compression is not necessary. On 64-bit JVMs, compression is enabled by default when the heap size is not too large (typically ≤ 32 GB), allowing 4-byte references to be used. 
    For arrays, an additional 4-byte field stores the array length, giving the JVM constant-time access to the number of elements.

This design makes object headers both powerful and costly: they enable the JVM to manage objects efficiently, but they also add fixed memory overhead to every object instance. That overhead is exactly what the Compact Object Headers feature seeks to reduce.

Memory Impact and GC Implications of Object Headers

Although small, object headers are present in every object. On a 64-bit JVM with uncompressed references, a typical non-array object carries 16 bytes of header overhead before any field data. 

For applications that allocate millions of short-lived or small objects, this fixed overhead can dominate heap usage. For example, 10 million objects would consume approximately 160 MB of heap space solely for headers, excluding the actual payload. 

Even with compressed references and class pointers, which reduce the footprint, headers remain a substantial per-object cost. This overhead effectively limits the amount of valuable data that can fit in memory, especially in memory-constrained environments such as containers or microservices.

Object headers are also integral to the JVM’s garbage collectors and synchronization mechanisms. The GC age fieldguides generational collectors in deciding when to promote objects from the young to the old generation. During compaction, a GC may overwrite the Mark Word with a forwarding pointer, allowing the JVM to relocate objects and update references transparently. At the same time, the header encodes lock state bits that must be coordinated with synchronization operations, while also storing identity hash codes when necessary. This heavy overloading of the header’s bits means that garbage collection, synchronization, and hashing all depend on this tiny but critical structure. 

Optimizations like Compact Object Headers, therefore, require careful engineering to preserve these semantics while reducing memory overhead. Now, let’s move from theory to practice using Java Object Layout (JOL).

Exploring Object Layout with JOL

To move from theory to practice, we can inspect objects in memory using Java Object Layout (JOL), an OpenJDK tool designed to reveal the actual memory structure of Java objects. 

JOL prints details such as the Mark Word, the Klass pointer, field offsets, and any padding introduced for alignment. This makes it an excellent way to verify how much memory an object really consumes and to observe the impact of features like Compact Object Headers.

To use JOL, include its Maven dependency in the project:

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.17</version>
  <scope>test</scope>
</dependency>

Following is the code we will use to study the memory layout of objects:

import org.openjdk.jol.info.ClassLayout;

public class JolDemo {

  static class Point {
    int x;
    int y;
  }

public static void main(String[] args) {
    Point p = new Point();
    System.out.println(ClassLayout.parseInstance(p).toPrintable());
  }
}

This small program defines a simple Point class with two integer fields and uses JOL’s ClassLayout to print its memory layout. For more information about JOL, consult the JavaDoc at: https://javadoc.io/doc/org.openjdk.jol/jol-core/latest/index.html. 

The Point class is deliberately minimal, making it easy to see how the JVM structures an object in memory without distractions from inheritance or complex fields.

Let’s run the program with the compact headers not enabled using the following command.

java -classpath /path/jol-core-0.17.jar JolDemo

Figure 1 shows the program’s output using JOL, illustrating the layout of a traditional object header.

The output displays the memory layout of the Point object when Compact Object Headers are not enabled. Let’s explore it:

  • Offset 0–7 (8 bytes): The Mark Word, which stores metadata such as the object’s hash, GC age, and lock state.
  • Offset 8–11 (4 bytes): The Class Pointer, which references the metadata of the Point class.
  • Offset 12–15 (4 bytes): The field int x.
  • Offset 16–19 (4 bytes): The field int y.
  • Offset 20–23 (4 bytes): An alignment gap, added so the total size of the object is a multiple of 8 bytes.

Figure 2 illustrates the traditional object layout (24 bytes), comprising a header, fields, and alignment.

The instance size is 24 bytes: 16 bytes for the header, 8 bytes for the two integer fields, and 4 bytes of external alignment padding. This illustrates how the traditional object header significantly contributes to the overall object size, especially when the fields are small.

Now let’s rerun the program, this time with Compact Object Headers enabled, by adding the option -XX:+UseCompactObjectHeaders to the command line.

java <strong>-XX:+UseCompactObjectHeaders</strong> -classpath /path/jol-core-0.17.jar JolDemo

Figure 3 shows the JOL output of the program, illustrating the memory layout of an object when Compact Object Headers are enabled.

This output shows the memory layout of the Point object when Compact Object Headers are enabled.

  • Offset 0–7 (8 bytes): The Mark Word, now compacted using the new layout (here identified as Lilliput). It embeds both the usual metadata (hash, GC age, lock state) and a reference to the class, eliminating the need for a separate class pointer field.
  • Offset 8–11 (4 bytes): The field int x.
  • Offset 12–15 (4 bytes): The field int y.

Figure 4 illustrates the memory layout of an object with Compact Object Headers enabled, occupying only 16 bytes and consisting of the header together with the object’s fields.

The instance size is 16 bytes, compared to 24 bytes without compact headers. This reduction comes from removing the separate 4-byte class pointer and avoiding the extra 4-byte alignment gap.

With Compact Object Headers, every object becomes smaller, and in applications with millions of objects, this leads to significant heap savings and reduced garbage collection pressure.

Exploring and implementing the Benchmark

Understanding the theory behind Compact Object Headers is essential, but nothing demonstrates their impact more effectively than a hands-on experiment. To explore this feature, I designed a benchmark that deliberately creates a large number of small objects, where header overhead is most visible.The benchmark instantiates 10 million Point objects, each containing just two integer fields. This setup minimizes the payload size of each object, making the relative cost of the header more significant. Once allocated, the program pauses briefly, giving us time to inspect memory usage with tools such as jcmd or VisualVM.

import java.util.ArrayList;
import java.util.List;
public class CompactHeaderBenchmark {

  static class Point {
    int x;
    int y;
  }
  public static void main(String[] args) throws 
                                    InterruptedException {
    List<Object> objects = new ArrayList<>();
    
    for (int i = 0; i < 10_000_000; i++) {
      objects.add(new Point());
    }
    
    System.out.println("Allocated 10 million objects");
    Thread.sleep(100_000);
  }
}

Running the benchmark

To evaluate the effect of Compact Object Headers, the same program, CompactHeaderBenchmark, is executed twice under JDK 25, once with the feature not enabled and once with it enabled. Let’s run the program first without the feature and analyze it. 

The following command runs the program without the feature:

java -Xms128m -Xmx512m \
     -cp . CompactHeaderBenchmark

Figure 5 illustrates the memory consumption of the program when Compact Object Headers are not enabled.

We can see in Figure 5 the heap usage graph of a Java program running with Compact Object Headers not enabled. In this execution, the JVM committed approximately 536 MB of heap, of which around 370 MB were actively used. 

The chart illustrates how the JVM must allocate a larger portion of the heap to store the objects, reflecting the overhead of the traditional object header layout (Mark Word plus a separate Class Pointer). 

The orange area at the top represents the total committed heap. In contrast, the blue area indicates the portion actually in use, with the gap between them highlighting reserved but unused capacity. 

Let’s check the same program with Compact Object Headers enabled. The following command runs the program with the Compact Object Headers feature:

java -Xms128m -Xmx512m \
     -XX:+UseCompactObjectHeaders \
     -cp . CompactHeaderBenchmark

Now, we can see in Figure 6 the heap usage graph of a Java program running with Compact Object Headers enabled.

In this execution, the JVM committed approximately 376 MB of heap, of which about 325 MB were actively used. Compared to the traditional header layout, the JVM both commits and uses less heap memory to hold the same workload. The orange area represents the total committed heap, while the blue area indicates the portion currently in use. 

By reducing the per-object overhead, Compact Object Headers enable more objects to fit into a smaller memory footprint. Now, let’s check the benchmark results and analysis.

Benchmark Results and Analysis

Running the benchmark on JDK 25 (macOS AArch64) with and without Compact Object Headers enabled highlighted the tangible memory savings this feature provides.

Figure 7 shows the heap usage comparison between traditional and compact object headers.

The traditional object header layout indicates that the JVM is committing approximately 536 MB of heap, of which 370 MB are actively used. With Compact Object Headers enabled, the committed heap drops to about 376 MB, with 325 MB in use.

These results demonstrate how Compact Object Headers reduce the memory footprint of object-dense workloads by shrinking the per-object overhead. 

While the savings per object are small, they accumulate quickly: in this benchmark, the reduction was around 30% less heap reserved and 12% less memory used. 

In large-scale applications with millions of objects, these gains can significantly reduce memory pressure, lower garbage collection overhead, and improve overall efficiency in memory-constrained environments, such as those found in containers and microservices.

These results are consistent with the design goals of Compact Object Headers, which aim to reduce the per-object overhead in memory-intensive workloads.

CONCLUSION

Compact Object Headers are one of those JVM enhancements that may seem invisible at first, yet they deliver a measurable impact in the right workloads. By shrinking the space required for each object header, JEP 519 enables applications to store more objects within the same heap, significantly reducing committed memory and easing the load on garbage collection.

In our benchmark with 10 million Point objects on JDK 25, the results showed up to 30% less committed memory and 12% less used memory, confirming that the feature works as intended. While the benefits are smaller in applications dominated by large objects, they are substantial in object-dense environments, such as microservices, caches, and data processing pipelines —the kinds of workloads where memory efficiency directly translates into lower costs and higher throughput.

Introduced experimentally in JDK 24 (JEP 450) and now promoted to a product feature in JDK 25 (JEP 519), Compact Object Headers highlight the JVM’s ongoing focus on efficiency without burdening developers with low-level details.

For developers and architects, the takeaway is clear: enabling this feature is a low-risk, high-reward optimization worth testing in production-like environments, especially for applications that run under memory constraints in the cloud or in containers. And while our benchmark focused on memory, JEP reports also show CPU gains of up to 8–10%, making Compact Object Headers a win for both memory and performance.

References AND SOURCE CODE

JEP 519: https://openjdk.org/jeps/519

JEP 450:  https://openjdk.org/jeps/450

JOL JavaDoc: https://javadoc.io/doc/org.openjdk.jol/jol-core/latest/index.html.

This article is part of the magazine issue Java 25 – Part 1.
You can read the complete issue with all contributions here.

Total
0
Shares
Previous Post

BoxLang AI v2.1.0 Released

Next Post

JAVA & JAKARTA EE and THE EVOLUTION OF THE CLOUD WITH NANOS UNIKERNEL…

Related Posts