
In diesem Artikel erfährst du, wie durch „Compressed Oops“ auf einem 64-Bit-System Referenzen auf Java-Objekte mit nur 32 statt 64 Bits dargestellt werden können und wie dadurch der Speicherbedarf einer Java-Anwendung signifikant reduziert wird.
Was ist ein 64-Bit-System?
Ein 64-Bit-System zeichnet sich dadurch aus, dass Pointer auf Adressen im Arbeitsspeicher 64 Bit groß sind. Dadurch lassen sich 264 Bytes = 16 Exabyte = 18.446.744.073.709.551.616 Bytes adressieren.
Heutzutage gibt es praktisch keine Programme mit diesem Speicherbedarf. In zehn Jahren werden wir über diese Aussage vielleicht schmunzeln ;-)
Hier die grafische Darstellung eines solchen 64-Bit-Pointers:

Nun könnte man überlegen, diese 64 Bit in der Mitte zu teilen und mit der gleichen Speichermenge nicht nur einen, sondern zwei Pointer zu speichern:

Mit 32 Bit lassen sich allerdings nur 232 Bytes = 4 GB adressieren. Das wiederum ist für viele Anwendungen zu wenig.
Die JDK-Entwickler:innen haben sich daher einen cleveren Trick („Compressed Oops“) einfallen lassen, um mit 32 Bit nicht nur 4 GB, sondern 32 GB zu adressieren. Und das wiederum ist für die meisten Anwendungen unserer Zeit ausreichend.
Wie funktionieren Compressed Oops?
Durch Compressed Oops (OOP steht für „ordinary object pointer“, der komprimierte Pointer heißt im HotSpot-Quellcode narrowOop) kann mit 32-Bit-Pointern ein Adressraum von 235 Bytes = 32 Gigabyte adressiert werden.
Wie ist das möglich? Um 32 Gigabyte zu adressieren, bräuchten wir doch 35 Bits. 35 Bits können wir aber nicht in einem 32-Bit-int speichern, dafür bräuchten wir die nächstgrößere Datenstruktur, ein 64-Bit-long:

Wir hätten also 29 Bit verschwendet. Dann hätten wir auch bei 64-Bit-Pointern bleiben können.
Es wird Zeit, einen Trick anzuwenden!
Zunächst einmal positionieren wir alle Java-Objekte an durch acht teilbare Speicheradressen. Acht ist 2³, d. h. die letzten 3 Bits eines Pointers sind immer 0. Somit enthalten nur die oberen 32 Bit relevante Informationen:

Da wir wissen, dass die letzten drei Bits immer 0 sind, brauchen wir diese nicht jedes Mal mit abzuspeichern. Die 35-Bit-Speicheradresse kann daher ohne Informationsverlust um 3 Bit nach rechts verschoben und somit in einem 32-Bit-Feld gespeichert werden:

Um später wieder auf die unkomprimierte Speicheradresse zugreifen zu können, müssen die 32 Bit lediglich wieder um 3 Bit nach links geschoben werden.
Die vier Modi von Compressed Oops
Die Rechnung oben setzt voraus, dass der Heap bei Adresse 0 beginnt. Wo der Heap tatsächlich liegt, hängt davon ab, welche Adressbereiche das Betriebssystem der JVM beim Start überlässt.
Die JVM versucht deshalb nacheinander, den Heap unterhalb von 4 GB, unterhalb von 32 GB und an einer durch 32 GB teilbaren Adresse zu reservieren. Gelingt nichts davon, nimmt sie eine beliebige Adresse.
Je nachdem, wo der Heap landet, arbeitet die JVM in einem von vier Modi. Die Tabelle zeigt pro Modus seinen Namen im Log der JVM, wo der Heap liegt und wie die JVM aus dem komprimierten Pointer die Speicheradresse berechnet. Die Heap-Basis ist dabei die Startadresse des reservierten Heap-Bereichs:
| Modus (Name im Log) | Lage des Heaps | Berechnung der Adresse |
|---|---|---|
32-bit | endet spätestens bei 4 GB | keine – der 32-Bit-Wert ist die Adresse |
Zero based | endet spätestens bei 32 GB | Shift um 3 Bit nach links |
Non-zero disjoint base | Heap-Basis durch 32 GB teilbar | Shift, dann Oder-Verknüpfung mit der Heap-Basis (x86, ARM: Addition) |
Non-zero based | beliebig | Shift, dann Addition der Heap-Basis |
Das folgende Bild zeigt den Modus Non-zero based: Der Shift macht aus dem komprimierten Pointer einen 35-Bit-Offset, und erst die Addition der Heap-Basis ergibt die 64-Bit-Speicheradresse.
Im Modus Non-zero disjoint base sind die unteren 35 Bits der Heap-Basis alle 0. Ihre Bits überschneiden sich also nicht mit denen des verschobenen Pointers, und die Oder-Verknüpfung (in Java: |) liefert dasselbe Ergebnis wie die Addition.
Im folgenden Bild steht die Heap-Basis deshalb nur in den oberen 29 Bits, der verschobene Pointer in den unteren 35 – die Oder-Verknüpfung setzt beide nebeneinander:
Die Oder-Verknüpfung nutzt die JVM nur auf PowerPC und IBM Z, wo sie die Heap-Basis nicht ständig in einem Register hält, sondern bei Bedarf als Konstante lädt – dort spart sie beim Dekodieren Befehle. Auf x86 und ARM bringt der Modus nichts, denn dort liegt die Heap-Basis in einem eigens dafür reservierten Register: Auf ARM erledigt ein einziger Befehl Shift und Addition, und auf x86 steckt beides im Speicherzugriff selbst.
Welchen Modus deine JVM gewählt hat, zeigt dir die Log-Option -Xlog:gc+heap+coops=debug:
java -Xmx2g -Xlog:gc+heap+coops=debug -version
Mit JDK 27 auf macOS (arm64) gibt der Befehl u. a. diese Zeile aus:
[0.010s][debug][gc,heap,coops] Heap address: 0x0000007000800000, size: 2048 MB, Compressed Oops mode: Non-zero disjoint base: 0x0000007000000000, Oop shift amount: 3
Der Heap ist nur 2 GB groß und liegt trotzdem nicht unterhalb von 32 GB: Die Heap-Basis 0x7000000000 entspricht 448 GB. Weil 448 durch 32 teilbar ist, arbeitet die JVM hier im Modus Non-zero disjoint base – auf ARM also mit derselben Addition wie im Modus Non-zero based.
Compressed Oops sind standardmäßig aktiviert
Auf einem 64-Bit-System mit einem Heap von weniger als 32 GB sind Compressed Oops standardmäßig aktiviert.
Ab 32 GB – also bereits bei -Xmx32g – schaltet die JVM sie ab. Denn sie reserviert vor dem Heap noch eine geschützte Speicherseite, und Heap und Speicherseite zusammen müssen in die 32 GB passen, die Compressed Oops adressieren können. Erzwingst du Compressed Oops trotzdem mit -XX:+UseCompressedOops, meldet die JVM „Max heap size too large for Compressed Oops“ und startet ohne sie.
Compressed Oops können mit folgender VM-Option deaktiviert werden:
-XX:-UseCompressedOops
Warum sollte man das tun?
Das Kodieren und Dekodieren der Pointer kostet Zeit. Nicht viel, da die dafür benötigte Shift-Operation in der Regel in einem einzigen CPU-Zyklus durchgeführt werden kann – in den Modi mit Heap-Basis kommt eine Addition bzw. Oder-Verknüpfung hinzu. Doch wer das letzte Quäntchen Performance aus seiner Anwendung herauskitzeln und den dafür erhöhten Speicherbedarf in Kauf nehmen will (alle Pointer belegen 64 statt 32 Bits), kann diese Möglichkeit in Betracht ziehen.
Fazit
Mit Compressed Oops lassen sich auf einem 64-Bit-System mit einer Heapgröße von weniger als 32 GB Speicheradressen mit nur 32 statt 64 Bits kodieren. Da es auf jedes aktive Java-Objekt in einer Anwendung mindestens einen Pointer gibt, kann somit der Speicherbedarf einer Anwendung signifikant reduziert werden.
Was soll ich als Nächstes vertiefen? Das erfahre ich am besten durch dein Feedback: Eine Bewertung auf meinem ProvenExpert-Profil zeigt mir, welche Themen dir wichtig sind – und motiviert mich, weitere Artikel zu schreiben.




