Compact Object Headers Feature ImageCompact Object Headers Feature Image
HappyCoders Glasses

Java Compact Object Headers
(JEP 450)

Sven Woltmann
Sven Woltmann
Aktualisiert: 7. April 2025

Jedes Java-Objekt hat im Speicher ein den eigentlichen Daten vorangestellten Objekt-Header. Dieser enthält vor allem den Hash-Code des Objekts und die Information, von welcher Klasse das Objekt eine Instanz ist.

Der Objekt Header ist zum Stand von Java 24 96 Bit (12 Byte) groß – oder 128 Bit (16 Byte), wenn Compressed Class Pointers ausgeschaltet werden (wozu es aber nahezu keinen Grund gibt).

Im Rahmen von Project Lilliput tüfteln die JDK-Entwickler seit vielen Jahren an Möglichkeiten, um den Header auf insgesamt 64 Bit oder sogar auf 32 Bit zu komprimieren.

In Java 24 war es dann soweit: Durch JDK Enhancement Proposal 450 wurden sogenannte „Compact Object Headers“ vorgestellt – zunächst im „Experimental“-Status. Compact Object Headers ermöglichen es, den Objekt-Header von 96 Bit auf 64 Bit zu komprimieren und dadurch die Heap-Größe bestehender Anwendungen signifikant zu reduzieren.

In diesem Artikel erfährst du:

  • Wie funktioniert die Header-Komprimierung?
  • Warum wird dadurch nicht nur der Speicherbedarf reduziert, sondern auch die Anwendungsperformance erhöht?

Status Quo

Eine detaillierte Beschreibung des Aufbaus von Objekt-Headern findest du im Hauptartikel über Java Object Header. Hier das Wichtigste zusammengefasst:

In der Regel besteht der Object Header aus einem 64-Bit „Mark Word“ und einem 32-Bit „Class Word“. Mark Word und Class Word sind wie folgt aufgebaut:

JEP 450: Java Object Header with Mark Word and Class Word

Das Mark Word enthält:

  • einen 31-Bit Identity Hash Code (der beim Aufruf von System.identityHashCode(Object) zurückgegeben wird),
  • 4 Bits, in denen der Garbage Collector das Alter eines Objekts speichert (anhand dessen er entscheidet, wann ein Object von der jungen in die alte Generation verschoben wird),
  • 2 „Tag Bits“, die anzeigen, ob das Objekt nicht, uncontended (ohne wartende Threads) oder contended (mit wartenden Threads) gelockt ist.

Bei Verwendung des veralteten Legacy Stack Lockings wurden im gelockten Zustand die ersten 62 Bit des Mark Words durch einen Pointer auf eine Lock-Datenstruktur ersetzt. Seit Java 23 hat das sogenannte Lightweight Locking diesen veralteten Mechanismus abgelöst.

Der alte Modus kann aktuell noch durch die VM-Option -XX:LockingMode=1 reaktiviert werden; er kann allerdings nicht mit Compact Object Headers kombiniert werden.

Das Class Word enthält einen 32-Bit-Offset in den maximal 4 GB großen Compressed Class Space, auf die sogenannte Klass-Datenstruktur, die alle relevanten Daten über die Klasse des Objekts enthält.

Wenn Compressed Class Pointers mit -XX:-UseCompressedClassPointers deaktiviert wurden, dann ist das Class Word 64 Bit groß und enthält einen unkomprimierten Pointer. Die Deaktivierung von Compressed Class Pointers kann ebenfalls nicht mit Compact Object Headers kombiniert werden.

Vom Compressed Class Pointer zum Compact Object Header

Wie können wir den Objekt-Header, ausgehend von Compressed Class Pointers, weiter komprimieren?

Zunächst einmal enthält das Mark Word (wie man oben sieht) aktuell 27 ungenutzte Bits (25 am Anfang und jeweils eines vor und nach den „Age Bits“). Von den 96 Bits des gesamten Object-Headers werden also nur 96 - 27 = 69 Bits benötigt. Um auf 64 Bit zu kommen, müssen wir also irgendwie fünf Bits einsparen.

Wo können wir die hernehmen?

Die JDK-Entwickler haben lange experimentiert, bis sie zu folgender Lösung kamen (ich habe für eine bessere Darstellung den Maßstab geändert – die 64 Bit ziehen sich jetzt über die gesamte Breite):

JEP 450: Compact Object Header

Der neue 64 Bit-Header wird nicht mehr in Mark Word und Class Word aufgeteilt, sondern enthält direkt die folgenden Informationen:

  • einen von 32 Bit auf 22 Bit weiter komprimierten Class Pointer (wird unten erklärt),
  • den 31-Bit Identity Hash Code (unverändert),
  • 4 für Project Valhalla reservierte Bits (neu),
  • 4 Bits für das Alter des Objekts (unverändert),
  • 1 Bit für das sogenannte „Self Forwarded Tag“ (wird unten erklärt),
  • 2 Tag Bits (unverändert).

Der Class Pointer wurde also um 10 Bits verkleinert. Da wir nur fünf Bits einsparen mussten, stehen nun fünf zusätzliche Bits zur Verfügung. Vier davon wurden für Projekt Valhalla reserviert, und in einem Bit wird das neue „Self Forwarded Tag“ gespeichert.

Wie konnte der Class Pointer auf 22 Bit komprimiert werden?

Mit den bisherigen 32 Bit konnte jede Position innerhalb des 4 GB großen Compressed Class Space einzeln adressiert werden.

Der Einfachheit halber stelle ich das in der folgenden Grafik mit einem 256 Byte großen Speicherbereich dar:

256 Byte großer Speicherbereich

Wie du siehst, brauchen wir die Zahlen 0 bis 255, um jede einzelne Position des Speicherbereichs zu adressieren. Dafür brauchen wir einen 8-Bit-Pointer (28 = 256).

Doch müssen wir wirklich jede einzelne Position adressieren können? Nein, das müssen wir nicht!

Genau wie eine Festplatte (egal ob eine herkömmlichen oder eine SSD) in sogenannte (in der Regel 4 KB große) Blöcke aufgeteilt ist, können wir auch den Speicherbereich für die Klassendaten in Blöcke aufteilen. So muss nicht mehr jedes einzelne Byte adressiert werden, sondern nur noch jeder Block. Und so können wir mit deutlich weniger Bits den gleichen Speicherbereich adressieren.

Hier wieder das vereinfachte Beispiel, in dem ich den 256 Byte großen Speicherbereich in 32 Blöcke zu je 8 Byte aufteile:

256 Byte großer Speicherbereich unterteilt in 8 Byte Blöcke

Jetzt brauchen wir nur noch die Zahlen 0 bis 31, um den gleichen Speicherbereich zu adressieren. Dafür brauchen wir nur noch 5 Bit große Pointer (25 = 32). Durch Aufteilung in Blöcke konnten wir den Speicherbedarf pro Pointer von 8 Bit auf 5 Bit reduzieren.

Und das funktioniert auch mit dem Speicherbereich, in dem die Klassen-Informationen liegen.

Bei der Verwendung von Compact Object Headers wird dieser Speicherbereich in 1.024 (= 210) Byte große Blöcke aufgeteilt. Dieser Wert wurde gewählt, da die meisten Klassen zwischen einem halben und einem Kilobyte belegen.

Zur Erinnerung: der Bereich ist 4 GB groß. Entsprechend ergeben sich 4 × 1.024 × 1.024 × 1.024 / 1.024 Blöcke, also 4 × 1.024 × 1.024, das sind 4.194.304, oder 222 Blöcke. Und diese können wir mit 22 Bit adressieren!

Um aus einer 22-Bit-Blocknummer einen Pointer zu machen, müssen wir die 32 Bit lediglich um 10 Bit nach links schieben und die hinteren 10 Bit mit Nullen auffüllen, und damit haben wir wieder einen 32-Bit-Pointer in den 4 GB großen Speicherbereich:

22-Bit Blocknummer wird zu einem 32-Bit Class Pointer

Die Aufteilung in Blöcke führt nun zu einer Fragmentierung bei den Klassendaten. Doch auch das haben die JDK-Developer bedacht: der zwischen den Klassen liegende Speicher kann auch von anderen Datenstrukturen des Metaspace genutzt werden.

Was ist das „Self Forwarded Tag“?

Wenn ein Garbage Collector ein Objekt an eine neue Speicheradresse kopiert, ersetzt er im ursprünglichen Objekt die oberen 62 Bit des Mark Words durch einen Pointer auf die neue Speicheradresse und setzt die Tag Bits auf 0x11. Das ursprüngliche Mark Word findet er dann an der neuen Adresse.

Wenn der Kopiervorgang fehlschlägt, wird das Mark Word durch einen Pointer auf das Objekt selbst ersetzt. Dadurch gehen Identity Hash Code und Alter des Objekts verloren, das scheint aber verschmerzbar zu sein (ich konnte leider keine verlässliche Information darüber finden, warum das der Fall ist, werde diesen Absatz aber aktualisieren, sollte ich eine Aussage hierzu finden).

Wenn wir allerdings einen Compact Object Header durch eine Selbst-Referenz ersetzen würden, dann würde auch der Class-Pointer verloren gehen. Da dieser essentiell ist, darf ein Compact Object Header eben nicht durch solch eine Selbst-Referenz ersetzt werden.

Stattdessen wird das neue „Self Forwarded Tag“-Bit gesetzt.

Fazit zu Compact Object Headers

Compact Object Headers reduzieren den Speicherbedarf eines Java-Programms signifikant, indem die Objekt-Header von 96 Bits (12 Bytes) auf 64 Bit (8 Bytes) reduziert werden.

Und nicht nur das – dadurch, dass die Objekte kleiner sind, passen auch mehr Objekte in den CPU-Cache. So kommt es zu weniger Cache-Misses – und das wirkt sich zusätzlich positiv auf die Performance aus.

Compact Object Headers befinden sich noch im experimentellen Stadium und müssen mit folgender VM-Option aktiviert werden:

-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders

Hast du Compact Object Headers bereits getestet? Hat es die erwarteten Verbesserungen gebracht? Teile deine Erfahrung in den Kommentaren!

Möchtest du informiert werden, sobald Compact Object Headers finalisiert und für die Produktion einsatzbereit sind? Dann klicke hier, und melde dich für den HappyCoders-Newsletter an, in dem ich dich regelmäßig über die neusten Entwicklungen aus der Java-Welt auf dem Laufenden halte.