HotSpot ist eine unter dem Namen Java HotSpot Performance Engine[3] veröffentlichte, sehr weit verbreitete Java Virtual Machine von Oracle (vorher von Sun Microsystems) für Arbeitsplatzrechner und Server. Sie basiert auf Techniken wie Just-in-time-Kompilierung und adaptiver Optimierung, um das Laufzeitverhalten von Software während der Ausführung zu verbessern.

HotSpot
Basisdaten

Hauptentwickler Oracle
Entwickler Oracle
Erscheinungsjahr 1999[1]
Aktuelle Version 22
(12. Dezember 2011)
Betriebssystem plattformübergreifend
Programmiersprache C++[2], Assemblersprache
Kategorie Java Virtual Machine
Lizenz GNU General Public License
openjdk.java.net/groups/hotspot/

Entstehungsgeschichte Bearbeiten

Der Ursprung von HotSpot geht auf eine Gruppe von Programmierern zurück, die Anfang der 1990er-Jahre bei Sun Microsystems an der Programmiersprache Self gearbeitet hatten. Diese Programmierer Lars Bak, Gilad Bracha, Steffen Grarup, David Griswold, Robert Griesemer und Urs Hölzle verließen 1994 Sun und gründeten gemeinsam eine Firma namens Longview Technologies, die auch unter dem Namen Animorphic Systems auftrat, um dort die auf Smalltalk (Programmiersprache) basierte optional typisierte Programmiersprache Strongtalk zu entwickeln. Dort gelang Urs Hölzle die Entwicklung eines Type-Feedback-Compilers, und auch die Strongtalk-VM machte große Fortschritte.[4] Aufgrund der stürmischen Entwicklung der neuen von Sun herausgebrachten Programmiersprache Java entschloss man sich eine grundlegend neue Java Virtual Machine zu entwickeln. In dieser Zeit wurde Srdjan Mitrovic aufgenommen, der für die Integration von Java in die neue VM sorgte. 1997 kaufte Sun[5] Longview Technologies[6] und Lars Bak wurde führender Ingenieur der Hotspot-VM bei Sun. Zum Zeitpunkt der Übernahme war HotSpot rund doppelt so schnell wie damals am Markt befindliche JVMs.[7][8] Sun Microsystems seinerseits wurde 2010 von Oracle übernommen.

Überblick Bearbeiten

Der Sun Java Server (auch Opto oder C2) Compiler verwandelt den Java Bytecode der Java Virtual Machine (JVM) in ausführbaren Maschinencode der entsprechenden Zielplattform. Es handelt sich um einen hochoptimierenden Just-in-time-Compiler (JIT). Just-in-time-Compiler erzeugen den Maschinencode im Gegensatz zu Ahead-of-time-Compilern wie beispielsweise GCC erst zur Laufzeit des Programms. Damit schlägt der Übersetzungsvorgang selbst, neben der eigentlichen Laufzeit des Programms, in Form von CPU-Zyklen und längerer Laufzeit zu Buche. Da dieser (Kompilier-)Vorgang allerdings nur adaptiv, das heißt bei einigen Methoden (so genannten Hotspots) angewandt wird, dieser Aufwand einmalig und insbesondere bei langlaufenden Serveranwendungen sehr kurz verglichen mit der Programm-Ausführungszeit ist, wird dieser Mehraufwand schnell durch die höhere Qualität des erzeugten Maschinencodes kompensiert. Außerdem erfolgt die Kompilierung in einem gesonderten Thread, der auf SMP-Maschinen normalerweise auf einer gesonderten CPU ausgeführt wird.

Zu Beginn der Laufzeit eines Java-Programms wird der gesamte Bytecode ausschließlich interpretiert. Das geschieht in einer Interpreterschleife, die Bytecode für Bytecode abarbeitet. Die Besonderheit von Hotspot-Compilern liegt nun darin, nur häufig benutzte Codeabschnitte – so genannte „Hotspots“ – sukzessive in Maschinencode zu übersetzen. Die Hotspots werden unter anderem auch lokal, das heißt innerhalb von Methoden, entdeckt, die kleinste Kompiliereinheit ist aber in jedem Fall die Methode. Da die Übersetzung nur bei besonders häufig genutzten oder langlaufenden Methoden und nicht mit dem gesamten Code geschieht, ist der Mehraufwand speziell bei langlaufenden Anwendungen vernachlässigbar.

Der Vorteil dieses Verfahrens liegt in der Möglichkeit, Closed-world-Optimierungen durchzuführen sowie Dynamische Optimierungen anwenden zu können. Dies bedeutet, dass die JVM immer den gesamten geladenen Bytecode kennt und anhand dessen Optimierungsentscheidungen treffen kann, welche die Semantik des Codes so verändern, dass dieser nur mehr unter den vorhandenen Einsatzbedingungen korrekt funktioniert. Wird zur Laufzeit Code geladen, welcher diese Einsatzbedingungen ändert, so führt die (Server-)Hotspot-VM eine Deoptimierung durch – und garantiert somit die korrekte Funktion, indem es den für diesen Einsatz zu aggressiv optimierten Code entfernt und erneut optimiert.

Der Kompiliervorgang des Sun-Hotspot-Compilers setzt sich aus den folgenden Schritten zusammen:

Hotspot-Erkennung, Compile Policy Bearbeiten

Das grundsätzliche Verfahren, wie und unter welchen Umständen eine Methode übersetzt wird, wird in der Sun JVM durch so genannte CompilePolicies definiert. Diese in einer C++-Klassenhierarchie modellierten Richtlinien sind im Fall des Server-Compilers in der Klasse StackWalkCompPolicy gespeichert. Somit kann durch Wechseln beziehungsweise Anpassen der Richtlinien der Kompiliervorgang umkonfiguriert werden. Das kommt in der Praxis allerdings relativ selten vor, vom Setzen des Flags -XX:+CompileTheWorld zum Übersetzen buchstäblich aller Methoden einmal abgesehen. Der Name StackWalkCompPolicy geht auf den Algorithmus zurück, den Aufrufstack der zu untersuchenden Methoden solange nach oben zu prüfen, bis die erste Methode entdeckt wird, die nicht mehr zu einem Inlining führen wird.

Im Servercompiler werden zur Erkennung der Hotspots zwei Schwellwerte herangezogen:

Aufrufhäufigkeit einer Methode
Dieser Schwellwert wird in der JVM-internen Variable CompileThreshold gehalten und hat auf den verschiedenen Plattformen unterschiedliche Grundeinstellungen. Auf der Intel-386-Architektur sind es bei der Server-JVM 10.000 und bei der Client-JVM 1.500 Methodenaufrufe.
Anzahl von Schleifendurchläufen in einer Methode
Werden Schleifen in Methoden allzu häufig durchlaufen, markiert dies eine Methode ebenfalls zur Kompilierung. Die Häufigkeit eines Schleifendurchlaufs wiederum wird in so genannten Backedge-Countern für jede Schleife einzeln mitgezählt.

Im Servercompiler befinden sich bereits Vorkehrungen, in späteren Versionen eine so genannte Two-tier-Kompilierung zu unterstützen. Dabei geht es darum, eine Methode zuerst zügig und ohne massive Optimierung zu kompilieren und später (oder parallel) in einem weiteren Durchlauf hochoptimiert zu übersetzen. Diese „two tier compilation“ wird perspektivisch zu einer Verschmelzung von Server und Client-Compiler führen.

Kompilierung Bearbeiten

Nachdem die Policy-Steuerung einzelne Methoden nun zum Kompilieren markiert hat, wird für jede der Methoden ein CompileTask Object erzeugt und in die CompileQueue eingereiht. Das, sowie die sonstige Kontrolle des gesamten Übersetzungsvorgangs, obliegt dem CompileBroker. Dieser erzeugt die einzelnen Übersetzungsjobs (CompileTask), stellt sie in die Queue und weckt zuletzt mit einem notify() inaktive Compile Threads. Die Anzahl der Compile Threads ist im Normalfall log(Anzahl der CPUs), doch mindestens zwei. Im CompileBroker werden auch diverse PerfCounter-Objekte gehalten, die zur späteren Leistungsüberwachung beziehungsweise -anzeige herangezogen werden können.

Nun wird der Bytecode in verschiedenen Phasen umgewandelt. Die wichtigsten Schritte sind hierbei:

  • Parsen des Bytecodes
  • Löschen ungenutzter Knoten (nodes)
  • Auswählen der Instruktionen.
  • Global Value Numbering (GVN)
  • Control Flow Graph (CFG) erzeugen
  • Register Allocation (Chaitin Graph Coloring)
  • Peephole-Optimierung

Die Internal Representation (IR) des Programmlaufs wird im SSA-Format gespeichert.

Optimierung Bearbeiten

Unter anderem durch die Speicherung des Knoten-Graphs im SSA-Format ist es möglich, eine Reihe von Optimierungsverfahren anzuwenden. Hier die wichtigsten:

Inlining
Kurze Methoden (maximal 35 Bytes) werden in den Rumpf des Aufrufers eingefügt, anstatt angesprungen zu werden. Bei längeren Methoden rentiert sich Inlining nicht.
Loop-Unrolling
Kurze Schleifen werden sozusagen „entrollt“, das heißt, die einzelnen Schleifendurchläufe werden sequenziell ohne Rücksprung abgearbeitet. Das vergrößert ähnlich dem Inlining den Speicherverbrauch, ist aber bei wenigen Schleifendurchläufen billiger als das ständige Testen einer Sprungbedingung.
Dead Code Elimination
Ungenutzte Anweisungen werden auf Bytecodeebene entdeckt und verworfen. Obwohl diese Optimierung auf Quellcodeebene durch den Java Frontend Compiler (javac) weit mehr Anwendung findet, wird dieser Schritt auch auf Bytecodeebene angewendet.
Peephole-Optimierung
Optimierung auf Assemblerebene. Hierbei wird ein Kontext (peephole) über einige wenige Assembler-Instruktionen erzeugt und zum Beispiel redundante Speicherzugriffe eliminiert und Registerzugriffe optimiert.

Codegenerierung, Aktivierung Bearbeiten

AD-Dateien Bearbeiten

Der Codegenerator (Emitter) des Systems erzeugt Maschinencode auf der Basis von vorgefertigten Schablonen, die zu dem Bytecode korrespondierende Assemblercodestrecken in so genannten Architecture Description Files (AD-Dateien) speichern. In diesen Dateien werden die verschiedenen CPUs abstrakt mit ihren Assemblerbefehlen, Adressiermodi und besonders der Anzahl und Breite der Register beschrieben. Die AD-Dateien werden mittels eines speziellen Preprozessors in C++-Klassen übersetzt, die in den VM-Buildprozess einfließen.

CompileCache Bearbeiten

Die von dem Compiler erstellten Assemblersprache-Instruktionen werden im CompilerCache mit einer Referenz auf den ursprünglichen Bytecode abgelegt. Das ist notwendig, um bei einer späteren Deoptimierung wieder auf den Bytecode zugreifen zu können. Deoptimierung kann nötig werden, wenn etwa dynamisch geladene Klassen, in denen einzelnen Methoden eingebettet wurden (Inlining), zur Laufzeit durch neue ersetzt werden.

Stubs Bearbeiten

Der durch den Kompiliervorgang erzeugte Maschinencode kann ohne Inanspruchnahme von Services der VM nicht funktionieren. In einer Methode muss zum Beispiel immer wieder ein name lookup in der Symboltabelle vorgenommen, eine Referenz aufgelöst oder andere Dienstleistungen der JVM in Anspruch genommen werden. Deswegen werden in den Maschinencode immer wieder so genannte Stubs eingefügt, durch die der kompilierte Code quasi Kontakt zur Außenwelt aufnimmt.

Aktivieren des Kompilats Bearbeiten

Grundsätzlich kann die fertiggestellte Methode entweder zur Laufzeit des Bytecodes ausgetauscht oder aber zum Zeitpunkt des nächsten Aufrufs verwendet werden. Das Unterbrechen einer laufenden Methode und Austauschen gegen die kompilierte Version nennt man On-Stack-Replacement (OSR). Um diese hochdynamische Operation zu bewerkstelligen, werden im Bytecode so genannte Savepoints eingefügt. An diesen Savepoints befindet sich die Virtual Machine in einem definierten, synchronisierten Zustand. Nun wird der Bytecode gegen das Kompilat getauscht und der genau entsprechende CPU-Stack synthetisch erzeugt.

Literatur Bearbeiten

  • Michael Paleczny, Christopher Vick, Cliff Click: The Java HotSpot server compiler. In: Proceedings of the Java Virtual Machine Research and Technology Symposium on Java Virtual Machine Research and Technology Symposium. Vol. 1. USENIX Association, Monterey, California 2001 (acm.org).
  • Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman: Compiler. Principles, Techniques, and Tools. Addison-Wesley, Reading 1988, ISBN 0-201-10194-7 (das „Dragon Book“).

Quellen Bearbeiten

  1. web.archive.org. 27. April 1999 (abgerufen am 2. November 2013).
  2. The hotspot Open Source Project on Open Hub: Languages Page. In: Open Hub. (abgerufen am 18. Juli 2018).
  3. The Java HotSpot Performance Engine Architecture. Abgerufen am 9. Juni 2020.
  4. The History of the Strongtalk Project von Dave Griswold.
  5. Sun buys Java compiler technology based upon Self!!! Keith Hankin 18. Februar 1997
  6. Engineers to make significant contributions to Sun's Java Platform (Memento vom 20. April 1999 im Internet Archive), Mountain View 18. Februar 1997
  7. Google 'Crankshaft' inspired by Sun Java HotSpot – Bak to 'adaptive compilation' von Cade Metz, 9. Dezember 2010
  8. Language Based Virtual Machines … or why speed matters (Memento vom 23. September 2015 im Internet Archive) von Lars Bak

Weblinks Bearbeiten