- HotSpot
-
HotSpot Entwickler Sun Microsystems Aktuelle Version 20 Betriebssystem plattformübergreifend Programmiersprache C/C++ Kategorie Java Virtual Machine Lizenz GNU General Public License Sun's OpenJDK HotSpot Website HotSpot ist der Name der sehr weit verbreiteten Java Virtual Machine von Sun Microsystems. Es basiert auf Techniken wie Just-in-time-Kompilierung und adaptiver Optimierung, um das Laufzeitverhalten von Software während der Ausführung zu verbessern.
Inhaltsverzeichnis
Überblick
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
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
Nachdem die Policy-Steuerung einzelne Methoden nun zum Kompilieren markiert hat, wird für jede der Methoden ein
CompileTask
Object erzeugt und in dieCompileQueue
eingereiht. Das, sowie die sonstige Kontrolle des gesamten Übersetzungsvorgangs, obliegt demCompileBroker
. Dieser erzeugt die einzelnen Übersetzungsjobs (CompileTask
), stellt sie in die Queue und weckt zuletzt mit einemnotify()
inaktive Compile Threads. Die Anzahl der Compile Threads ist im Normalfall log(Anzahl der CPUs), doch mindestens zwei. Im CompileBroker werden auch diversePerfCounter
-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
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
AD-Dateien
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
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 inlined wurden, zur Laufzeit durch neue ersetzt werden.Stubs
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
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.
Siehe auch
- Compiler optimization in der englischen Wikipedia
Literatur
- 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“).
Weblinks
- Michael Paleczny, Christopher Vick, Cliff Click: The Java HotSpot Server Compiler. Sun Microsystems (PDF-Datei, 111 kB, englisch)
- Sun Barcelona Virtual Machine (englisch)
- Java Virtual Machine Specification (englisch)
- Java SE HotSpot at a Glance (englisch)
Kategorie:- Java-Programmierwerkzeug
Wikimedia Foundation.