java object header and compressed class pointers hero imagejava object header and compressed class pointers hero image
HappyCoders Glasses

Java Object Headers und Compressed Class Pointers​

Sven Woltmann
Sven Woltmann
Aktualisiert: 4. Dezember 2024

Jedes Java-Objekt enthält im Arbeitsspeicher nicht nur die eigentlichen Daten, sondern dazu auch einen sogenannten „Object Header“, der vor den Daten steht. Dieser Header enthält z. B. den Identity Hash Code eines Objekts, Informationen über das Alter des Objekts und einen Verweis auf die Klasse, die durch dieses Objekt instanziiert wurde.

In diesem Artikel erfährst du:

  • Wie ist der Object Header aufgebaut?
  • Was sind Mark Word und Class Word?
  • Was ist der Compressed Class Space?
  • Wie können Compressed Class Pointers auf einem 64-Bit-System mit nur 32 Bit dargestellt werden?

Dieser Artikel beschreibt den Aufbau des Objekt-Headers zum Stand von Java 23, d. h. bevor dieser durch sogenannte „Compact Object Headers“ weiter komprimiert wird. Compact Object Headers werden im Rahmen von Project Lilliput erstmals in Java 24 aktiviert werden können.

Aufbau des Java Objekt-Headers

Der Java Object Header besteht aus einem sogenannten „Mark Word“ und einem „Class Word“. Auf einem 64-Bit-System mit nicht komprimierten Klassen-Pointern belegt der Header 128 Bit – also 16 Byte – und hat folgenden Aufbau:

Java Object Header: 64-Bit Mark Word und 64-Bit Class Word, gesamt: 128 Bit

Mit komprimierten Klassen-Pointern (was das genau bedeutet, erkläre ich dir im Abschnitt Compressed Class Pointers) ist das Class Word nur 32 Bit lang – und damit der gesamte Objekt-Header nicht mehr 128 Bit, sondern nur noch 96 Bit – also 12 Byte:

Java Object Header: 64-Bit Mark Word und 32-Bit komprimiertes Class Word, gesamt: 96 Bit

Welche Daten enthalten Mark Word und Class Word, und wie sind sie aufgebaut?

Mark Word

Schauen wir uns zunächst das Mark Word im Detail an (beachte, dass ich den Maßstab gegenüber den vorherigen Grafiken geändert habe, um die Details besser darzustellen):

Java Mark Word Layout

Das Mark Word enthält in der Regel die folgenden Informationen:

  • 25 ungenutzte Bits.
  • 31 Bit für den Identity Hash Code des Objekts – das ist der Wert, der durch System.identityHashCode(Object) zurückgegeben wird.
  • 1 ungenutztes Bit – dieses wurde durch den bereits in Java 14 entfernten Concurrent Mark Sweep (CMS) Garbage Collector bei Komprimierung des Class Words (s. u.) genutzt.
  • 4 Bits, in denen der Garbage Collector das Alter des Objekts speichert, anhand dessen er entscheidet, wann ein Objekt von der jungen in die alte Generation verschoben wird.
  • 1 ungenutztes Bit – dieses wurde für das in Java 15 deaktivierte Biased Locking verwendet.
  • 2 Bits für den Lock-Zustand des Objekts („Tag Bits“).

Beim veralteten „Legacy Stack Locking“ konnte sich das Mark Word auch ändern – wie, das erkläre ich im nächtsen Abschnitt.

Legacy Stack Locking

Beim „Legacy Stack Locking“ (dem Standard-Locking-Mechanismus bis Java 22) werden im gelockten Zustand (d. h. wenn sich ein Thread innerhalb eines synchronized-Blocks befindet, der auf diesem Objekt definiert wurde), die ersten 62 Bit des Mark Words durch einen Pointer auf eine zusätzliche Datenstruktur auf dem Stack ersetzt:

Java Mark Word in locked state

Diese Datenstruktur enthält dann wiederum das eigentliche Mark Word sowie weitere Informationen über das Lock, wie z. B. eine Liste der Threads, die blockiert wurden und darauf warten, den synchronized-Block betreten zu dürfen.

Der Pointer zu dieser separaten Datenstruktur ist übrigens einer von zwei Gründen für das Pinning bei virtuellen Threads: Ein virtueller Thread, der innerhalb eines synchronized-Blocks blockierenden Code aufruft, darf nicht vom Carrier Thread gelöst werden.

Denn wenn der virtuelle Thread danach auf einem anderen Carrier Thread weiterlaufen würde (dessen Stack an einer anderen Adresse im Speicher liegt), wäre dieser Pointer nicht mehr gültig.

Da das „Legacy Stack Locking“ den Zugriff auf die eigentlichen Daten des Mark Words erschwerte und mit ein Grund für das o. g. Pinning war, wurde es durch das modernere „Lightweight Locking“ ersetzt.

Lightweight Locking

Seit Java 21 gibt es das sogenannte „Lightweight Locking“, das ohne Änderung des Mark Words auskommt. Seit Java 23 ist dies der Standard-Modus.

Beim Lightweight Locking werden – sofern kein anderer Thread den kritischen Bereich betreten will – lediglich die Tag Bits (die letzten zwei Bits des Mark Words) von 0x01 (unlocked) auf 0x00 (lightweight-locked) geändert. Es wird keine zusätzliche Datenstruktur angelegt.

Erst wenn ein weiterer Thread versucht den kritischen Bereich zu betreten, wird das Lock „inflated“ (auf deutsch könnte man sagen: „aufgebläht“):

  • Eine zusätzliche Datenstruktur, die u. a. eine Liste der wartenden Threads enthält, wird erstellt.
  • Der Pointer auf diese Datenstruktur wird in einer separaten Hashtable abgelegt und nicht mehr im Mark Word – die Inflation des Locks wird dort lediglich durch die Änderung der Tag Bits auf 0x10 angezeigt.

Lightweight Locking ist also eine effizientere Möglichkeit zur Synchronisierung von Threads, indem es die Änderung des Mark Words überflüssig macht und den Overhead unnötiger Monitor-Objekte in Szenarien ohne Thread Contention (d. h. dass keine Threads auf andere warten) reduziert.

Class Word

Das Class Word (manchmal auch „Klass Word“) ist schnell erklärt:

Es enthält einen Pointer auf die sogenannte Klass-Datenstruktur im Metaspace – einem Speicherbereich außerhalb des Java-Heaps. Diese Datenstruktur enthält alle relevanten Informationen über die Klasse des Objekts. Alle Objekte derselben Klasse, z. B. alle ArrayList-Objekte, zeigen auf dieselbe Klass-Datenstruktur.

Auf einem 64-Bit-System ist auch dieser Pointer (sofern er nicht komprimiert wird – dazu kommen wir gleich) 64 Bit groß:

Java Class Word, nicht komprimiert, 64 Bits

Mit diesen 64 Bit lassen sich 16 EB (16 Exabyte = 18.446.744.073.709.551.616 Bytes) adressieren. Eine Klass-Datenstruktur ist in der Regel zwischen einem halben und einem Kilobyte groß. Mit 64 Bit könnten wir also 264 / 768 = 24 Billiarden Klassen referenzieren. Das ist eine Zahl, die vermutlich auch in 30 Jahren noch sehr groß erscheint.

Daher wurden der sogenannte „Compressed Class Space“ und „Compressed Class Pointers“ eingeführt, die ich in den nächsten zwei Abschnitten beschreiben werde.

Compressed Class Space

Der „Compressed Class Space“ ist ein zusammenhängender Speicherblock innerhalb des Metaspaces (einem Speicherbereich außerhalb des Heaps), in dem alle Klass-Datenstrukturen abgelegt sind. Dieser Bereich wird beim Start eines Java-Programms allokiert, und seine Größe kann sich während der Laufzeit nicht ändern.

Java Speicherlayout: Heap, Metaspace, Compressed Class Space, C Heap, Thread Stack

Standardmäßig ist der Compressed Class Space 1 GB groß. Seine Größe kann mit der folgenden VM-Option geändert werden:

-XX:CompressedClassSpaceSize=<size>

Erlaubt sind Werte zwischen 1 MB und 4 GB.

Der Name „Compressed Class Space“ ist irreführend, da nicht die Klass-Datenstrukturen selbst komprimiert sind, sondern die Pointer vom Class Word des Object Headers auf diese Klass-Datenstrukturen. Dazu mehr im nächsten Abschnitt.

Compressed Class Pointers

Wie im vorherigen Abschnitt erwähnt, kann der Compressed Class Space maximal 4 GB groß sein. Um 4 GB zu adressieren, genügen 32 Bit (232 = 4.294.967.296).

Ein Compressed Class Pointer ist somit ein 32-Bit-Wert, der die Adresse der Klass-Datenstruktur als Offset innerhalb des Compressed Class Spaces beschreibt:

Java Class Word, 32 Bits, mit Compressed Class Pointer

Die tatsächliche Adresse der Klass-Datenstruktur ergibt sich durch Addition der Startadresse des Compressed Class Spaces und dieses Offsets.

Compressed Class Pointers sind standardmäßig aktiviert

Auf einem 64-Bit-System sind komprimierte Klassen-Pointer standardmäßig aktiviert. Du kannst sie mit folgender Option deaktivieren:

-XX:-UseCompressedClassPointers

Es gibt allerdings keinen Grund das zu tun, es sei denn, man vermutet einen Bug in der Implementierung der Compressed Class Pointers als Ursache eines Problems in seiner eigenen Anwendung.

Bis Java 14 war die Aktivierung von Compressed Class Pointers an die Aktivierung von Compressed OOPs (komprimierte Objekt-Pointer) gekoppelt. Wenn man Compressed OOPs deaktivierte, wurden automatisch auch Compressed Class Pointers deaktiviert. Da es keinen Grund für diese Kopplung gab, wurde sie in Java 15 aufgehoben.

Ausblick: Compact Object Headers

Seit mehreren Jahren wird im Rahmen von Project Lilliput daran gearbeitet, die Objekt-Header in Java weiter zu verkleinern – zunächst auf 64 Bit, später eventuell sogar auf 32 Bit.

Der erste Meilenstein scheint bald erreicht zu sein. Schon in Java 24 wird durch JDK Enhancement Proposal 450 eine erste experimentelle Version von Compact Object Headers veröffentlicht und damit die Header-Größe auf 64 Bit reduziert werden.

Möchtest du mehr erfahren, wenn es soweit ist? 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.