Java 25 Compact Object Headers Explained

Introduction Every Java object begins with a hidden object header, which the JVM uses to manage it. On today's 64-bit JVMs that header has taken 12 bytes - often more than the object's actual data. A small object such as a Point with two int fields carries 8 bytes of data and 12 bytes of header, padded to 24 bytes in all. Java applications create billions of small objects, so headers can account for a large share of the heap. Project Lilliput set out to shrink them, and after an experimental release in Java 24, Java 25Click to read tutorial: Overview of New Features in Java 25 (JEP 519) made compact object headers a product feature: headers shrink to 8 bytes. This tutorial explains what object headers contain, measures the difference with a runnable example, and shows how to enable compact headers. What is in an object header Until now, a header on a 64-bit JVM had two parts -
  • the mark word (8 bytes) - the object's identity hash code, its age for the garbage collector, and bits used for locking with synchronized, and
  • the class pointer (4 bytes with compressed class pointers, the default) - which class the object belongs to.
Objects are aligned to 8 bytes, so a 12-byte header often forces padding. With compact headers, the class pointer is squeezed into the mark word: a compressed 22-bit class pointer plus the hash code, GC age and lock bits all fit into a single 8-byte word. To make that possible, the JVM changed how it stores locking and hash-code information for the rare objects which need more room, without changing the behaviour of Java code.
Measuring the difference The program below creates 5 million small objects, each holding two int fields, keeps them all reachable, and measures how much the used heap grew. That growth divided by the number of objects is the memory each object takes, including its header -
Measuring the memory per small object
//HeaderSize.java
public class HeaderSize {
  static final class Point {                 // 8 bytes of data
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
  }

  static long usedHeap() {
    for (int i = 0; i < 3; i++) System.gc();
    Runtime rt = Runtime.getRuntime();
    return rt.totalMemory() - rt.freeMemory();
  }

  public static void main(String[] args) {
    int count = 5_000_000;
    Point[] points = new Point[count];       // the array itself is allocated before measuring
    long before = usedHeap();
    for (int i = 0; i < count; i++) points[i] = new Point(i, i);
    long after = usedHeap();
    System.out.printf("Bytes per Point object: about %.1f%n", (after - before) / (double) count);
    System.out.println("Points kept: " + points.length);
  }
}
java HeaderSize
java -XX:+UseCompactObjectHeaders HeaderSize
 OUTPUT with standard headers
Bytes per Point object: about 23.9
Points kept: 5000000
 OUTPUT with compact object headers
Bytes per Point object: about 15.9
Points kept: 5000000
Explanation of the output
  • With standard headers, each Point takes 24 bytes: a 12-byte header, 8 bytes of fields, and 4 bytes of padding to reach a multiple of 8.
  • With compact headers, each takes 16 bytes: an 8-byte header and 8 bytes of fields, with no padding needed - a third less memory for these objects.
  • The saving depends on the object: it is largest for small objects, and for an object whose size was already padded, it can be zero. In real applications, the JEP reports heap reductions of around 10-20%, with corresponding reductions in garbage collection work, and faster execution in some benchmarks thanks to better use of CPU caches.
Enabling compact object headers
  • Java 24: experimental, enabled with -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders.
  • Java 25: a product feature, enabled with just -XX:+UseCompactObjectHeaders. It is not yet the default; that is expected in a future release once it has been widely used.
  • It works with all the garbage collectors (G1, Parallel, Serial, ZGC and Shenandoah), and requires no changes to application code.
  • A JVM can address a limited number of classes with the compressed 22-bit class pointer - about four million - which is far more than real applications load.
Since it only needs a command-line flag, compact object headers are easy to try: run your application's load tests with and without it, and compare heap usage and GC activity. Summary In this tutorial we saw that every Java object starts with a header holding its mark word and class pointer, which took 12 bytes on 64-bit JVMs. Java 25's compact object headers fit both into 8 bytes. We measured a small two-int object taking 24 bytes with standard headers and 16 bytes with compact headers, and saw how to enable the feature in Java 24 and Java 25. Next, read about the Key Derivation Function APIClick to read tutorial: Java 25 Key Derivation Function (KDF) API Tutorial.