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?
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:
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:
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):
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:
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.
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ß:
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.
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:
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.
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.