Kompilierungstypen in der JVM: Aufdecken der Black Magic-Sitzung

Hallo allerseits!

Heute wird Ihre Aufmerksamkeit auf eine Übersetzung des Artikels gelenkt, die Beispiele für Kompilierungsoptionen in der JVM zeigt. Besonderes Augenmerk wird auf die in Java 9 und höher unterstützte AOT-Kompilierung gelegt.

Viel Spaß beim Lesen!

Ich glaube, jeder, der jemals in Java programmiert hat, hat von Instant Compilation (JIT) und möglicherweise Compilation vor der Ausführung (AOT) gehört. Darüber hinaus muss nicht erklärt werden, was „interpretierte“ Sprachen sind. In diesem Artikel wird erläutert, wie all diese Funktionen in der Java Virtual Machine JVM implementiert sind.

Sie wissen wahrscheinlich, dass Sie beim Programmieren in Java einen Compiler (mit dem Programm "javac") ausführen müssen, der Java-Quellcode (.java-Dateien) in Java-Bytecode (.class-Dateien) sammelt. Java-Bytecode ist eine Zwischensprache. Es wird als "Zwischenprodukt" bezeichnet, da es von einem realen Computergerät (CPU) nicht verstanden wird und nicht von einem Computer ausgeführt werden kann und somit eine Übergangsform zwischen dem Quellcode und dem im Prozessor ausgeführten "nativen" Maschinencode darstellt.

Damit Java-Bytecode eine bestimmte Arbeit ausführen kann, gibt es drei Möglichkeiten, ihn dazu zu bringen:

  1. Führen Sie den Zwischencode direkt aus. Es ist besser und korrekter zu sagen, dass es "interpretiert" werden muss. Die JVM verfügt über einen Java-Interpreter. Wie Sie wissen, müssen Sie das Programm "Java" ausführen, damit die JVM funktioniert.
  2. Kompilieren Sie den Zwischencode kurz vor der Ausführung in nativen Code und zwingen Sie die CPU, diesen frisch gebackenen nativen Code auszuführen. Die Kompilierung erfolgt daher unmittelbar vor der Ausführung (Just in Time) und wird als „dynamisch“ bezeichnet.
  3. 3Die ersten Schritte, noch bevor das Programm gestartet wird, werden die Zwischencodes in native übersetzt und von Anfang bis Ende durch die CPU ausgeführt. Diese Kompilierung erfolgt vor der Ausführung und heißt AoT (Ahead of Time).

(1) ist also die Arbeit des Interpreters, (2) ist das Ergebnis der JIT-Kompilierung und (3) ist das Ergebnis der AOT-Kompilierung.

Der Vollständigkeit halber möchte ich erwähnen, dass es einen vierten Ansatz gibt - die direkte Interpretation des Quellcodes, aber in Java wird dies nicht akzeptiert. Dies geschieht beispielsweise in Python.
Nun wollen wir sehen, wie "Java" als (1) der Interpreter von (2) dem JIT-Compiler und / oder (3) dem AOT-Compiler funktioniert - und wann.

Kurz gesagt - in der Regel macht "Java" sowohl (1) als auch (2). Ab Java 9 ist auch eine dritte Option möglich.

Hier ist unsere Test , die in zukünftigen Beispielen verwendet wird.

 public class Test { public int f() throws Exception { int a = 5; return a; } public static void main(String[] args) throws Exception { for (int i = 1; i <= 10; i++) { System.out.println("call " + Integer.valueOf(i)); long a = System.nanoTime(); new Test().f(); long b = System.nanoTime(); System.out.println("elapsed= " + (ba)); } } } 

Wie Sie sehen können, gibt es eine main , die das Testobjekt instanziiert und die f Funktion zehnmal hintereinander zyklisch aufruft. Die f Funktion macht fast nichts.

Wenn Sie also den obigen Code kompilieren und ausführen, wird die Ausgabe durchaus erwartet (natürlich werden die Werte der verstrichenen Zeit für Sie unterschiedlich ausfallen):

 call 1 elapsed= 5373 call 2 elapsed= 913 call 3 elapsed= 654 call 4 elapsed= 623 call 5 elapsed= 680 call 6 elapsed= 710 call 7 elapsed= 728 call 8 elapsed= 699 call 9 elapsed= 853 call 10 elapsed= 645 

Und jetzt lautet die Frage: Ist diese Schlussfolgerung das Ergebnis der Arbeit von „Java“ als Interpreter, dh Option (1), „Java“ als JIT-Compiler, dh Option (2), oder hängt sie irgendwie mit der AOT-Kompilierung zusammen? das heißt, Option (3)? In diesem Artikel werde ich die richtigen Antworten auf all diese Fragen finden.

Die erste Antwort, die ich geben möchte, ist höchstwahrscheinlich, dass hier nur (1) stattfindet. Ich sage "höchstwahrscheinlich", da ich nicht weiß, ob hier eine Umgebungsvariable festgelegt ist, die die Standard-JVM-Optionen ändern würde. Wenn nichts Überflüssiges installiert ist und „Java“ standardmäßig so funktioniert, beobachten wir hier zu 100% nur Option (1), dh der Code wird vollständig interpretiert. Da bin ich mir sicher:

  • Gemäß der Java-Dokumentation wird die Option -XX:CompileThreshold=invocations mit den Standardaufrufen invocations=1500 auf der Client-JVM ausgeführt (weitere -XX:CompileThreshold=invocations zur Client-JVM werden unten beschrieben). Da ich es nur 10 Mal und 10 <1500 ausführe, sprechen wir hier nicht über dynamische Kompilierung. In der Regel gibt diese Befehlszeilenoption an, wie oft (maximal) die Funktion interpretiert werden muss, bevor der dynamische Kompilierungsschritt beginnt. Ich werde weiter unten darauf eingehen.
  • Tatsächlich habe ich diesen Code mit Diagnoseflags ausgeführt, sodass ich weiß, ob er dynamisch kompiliert wurde. Ich werde diesen Punkt auch weiter unten erläutern.

Bitte beachten Sie: JVM kann im Client- oder Servermodus arbeiten, und die standardmäßig im ersten und zweiten Fall festgelegten Optionen sind unterschiedlich. In der Regel wird die Entscheidung über den Startmodus automatisch getroffen, abhängig von der Umgebung oder dem Computer, auf dem die JVM gestartet wurde. Im Folgenden werde –client bei allen Starts die Option –client angeben, um nicht zu bezweifeln, dass das Programm im Client-Modus ausgeführt wird. Diese Option wirkt sich nicht auf die Aspekte aus, die ich in diesem Beitrag demonstrieren möchte.

Wenn Sie "Java" mit der -XX:PrintCompilation das Programm eine Zeile, wenn die Funktion dynamisch kompiliert wird. Vergessen Sie nicht, dass die JIT-Kompilierung für jede Funktion separat ausgeführt wird. Einige Funktionen in der Klasse verbleiben möglicherweise im Bytecode (dh nicht kompiliert), während andere möglicherweise bereits die JIT-Kompilierung bestanden haben, dh für die direkte Ausführung im Prozessor bereit sind .

Unten füge ich auch die Option -Xbatch . Die Option -Xbatch nur benötigt, um die Ausgabe präsentabler zu gestalten. Andernfalls wird die JIT-Kompilierung (zusammen mit der Interpretation) -XX:PrintCompilation , und die Ausgabe nach der Kompilierung kann zur Laufzeit manchmal seltsam aussehen (aufgrund von -XX:PrintCompilation ). Die Option –Xbatch deaktiviert jedoch die Hintergrundkompilierung. Daher wird die Ausführung unseres Programms vor dem Ausführen der JIT-Kompilierung gestoppt.

(Aus Gründen der Lesbarkeit werde ich jede Option aus einer neuen Zeile schreiben.)

 $ java -client -Xbatch -XX:+PrintCompilation Test 

Ich werde die Ausgabe dieses Befehls hier nicht einfügen, da die JVM standardmäßig viele interne Funktionen kompiliert (z. B. in Bezug auf Java-, Sun- und JDK-Pakete), sodass die Ausgabe sehr lang ist. Auf meinem Bildschirm befinden sich also 274 Zeilen in den internen Funktionen und noch ein paar mehr - bis zum Abschluss des Programms). Um diese Recherche zu vereinfachen, werde ich die JIT-Kompilierung für innere Klassen abbrechen oder sie selektiv nur für meine Methode Test.f ( Test.f ). -XX:CompileCommand eine weitere Option an: -XX:CompileCommand . Sie können viele Befehle angeben (Kompilierung), sodass es einfacher ist, sie in einer separaten Datei abzulegen. Zum Glück haben wir die Option -XX:CompileCommandFile . Fahren Sie also mit dem Erstellen der Datei fort. Ich werde es hotspot_compiler aus einem Grund nennen, den ich kurz erläutern und Folgendes schreiben werde:

 quiet exclude java/* * exclude jdk/* * exclude sun/* * 

In diesem Fall sollte völlig klar sein, dass wir alle Funktionen (das letzte *) in allen Klassen von allen Paketen ausschließen, die mit Java, JDK und Sun beginnen (Paketnamen werden durch / getrennt, und Sie können * verwenden). Der Befehl quiet weist die JVM an, nichts über die ausgeschlossenen Klassen zu schreiben, sodass nur die jetzt kompilierten Klassen an die Konsole ausgegeben werden. Also renne ich:

 java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Test 

Bevor ich Sie über die Ausgabe dieses Befehls hotspot_compiler , hotspot_compiler ich Sie daran erinnern, dass ich diese Datei hotspot_compiler , da anscheinend in Oracle JDK der Name .hotspot_compiler für die Datei mit Compilerbefehlen standardmäßig .hotspot_compiler ist (ich habe dies nicht überprüft).

Die Schlussfolgerung lautet also:

 many lines like this 111 1 n 0 java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static) call 1 some more lines like this 161 48 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native) (static) elapsed= 7558 call 2 elapsed= 1532 call 3 elapsed= 920 call 4 elapsed= 732 call 5 elapsed= 774 call 6 elapsed= 815 call 7 elapsed= 767 call 8 elapsed= 765 call 9 elapsed= 757 call 10 elapsed= 868 

Erstens weiß ich nicht, warum einige java.lang.invoke.MethodHandler. Methoden noch kompiliert werden java.lang.invoke.MethodHandler. Wahrscheinlich können einige Dinge einfach nicht ausgeschaltet werden. Soweit ich weiß, werde ich diesen Beitrag aktualisieren. Wie Sie sehen, sind jetzt alle anderen Kompilierungsschritte (zuvor waren es 274 Zeilen) verschwunden. In weiteren Beispielen werde ich auch java.lang.invoke.MethodHandler aus der Ausgabe des Kompilierungsprotokolls entfernen.

Mal sehen, wozu wir gekommen sind. Jetzt haben wir einen einfachen Code, in dem wir unsere Funktion 10 Mal ausführen. Ich habe bereits erwähnt, dass diese Funktion interpretiert und nicht kompiliert wird, wie in der Dokumentation angegeben. Jetzt sehen wir sie in den Protokollen (gleichzeitig sehen wir sie nicht in den Kompilierungsprotokollen, und dies bedeutet, dass sie keiner JIT-Kompilierung unterzogen wird). Nun, Sie haben gerade das Java-Tool in Aktion gesehen, das unsere Funktion nur in 100% der Fälle interpretiert und interpretiert. Wir können also das Kontrollkästchen aktivieren, das mit Option (1) herausgefunden wurde. Wir gehen zu (2), dynamischer Zusammenstellung.

Laut Dokumentation können Sie die Funktion 1.500 Mal ausführen und sicherstellen, dass die JIT-Kompilierung tatsächlich stattfindet. Sie können jedoch auch die -XX:CompileThreshold=invocations , wobei der gewünschte Wert anstelle von 1500 festgelegt wird. Lassen Sie uns hier 5 zeigen. Dies bedeutet, dass wir Folgendes erwarten: Nach 5 „Interpretationen“ unserer Funktion f muss die JVM die Methode kompilieren und dann die kompilierte Version ausführen.
java -client -Xbatch

 -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 Test 

Wenn Sie diesen Befehl ausgeführt haben, haben Sie möglicherweise festgestellt, dass sich im Vergleich zum obigen Beispiel nichts geändert hat. Das heißt, die Kompilierung findet immer noch nicht statt. Laut Dokumentation stellt sich heraus, dass -XX:CompileThreshold nur funktioniert, wenn TieredCompilation deaktiviert ist. TieredCompilation ist die Standardeinstellung. Es -XX:-TieredCompilation : -XX:-TieredCompilation . Tiered Compilation ist eine in Java 7 eingeführte Funktion, um sowohl die Start- als auch die Reisegeschwindigkeit der JVM zu verbessern. Im Kontext dieses Beitrags ist dies nicht wichtig. Deaktivieren Sie ihn daher. Lassen Sie uns diesen Befehl jetzt erneut ausführen:

 java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation Test 

Hier ist die Ausgabe (ich erinnere mich, ich habe die Zeilen bezüglich java.lang.invoke.MethodHandle verpasst):

 call 1 elapsed= 9411 call 2 elapsed= 1291 call 3 elapsed= 862 call 4 elapsed= 1023 call 5 227 56 b Test::<init> (5 bytes) 228 57 b Test::f (4 bytes) elapsed= 1051739 call 6 elapsed= 18516 call 7 elapsed= 940 call 8 elapsed= 769 call 9 elapsed= 855 call 10 elapsed= 838 

Wir begrüßen (hallo!) Die dynamisch kompilierte Funktion Test.f oder Test::<init> unmittelbar nach dem Aufruf von Nummer 5, da ich CompileThreshold auf 5 gesetzt habe. Die JVM interpretiert die Funktion fünfmal, kompiliert sie dann und führt schließlich die kompilierte Version aus. Da die Funktion kompiliert ist, sollte sie schneller ausgeführt werden. Dies können wir hier jedoch nicht überprüfen, da diese Funktion nichts bewirkt. Ich denke, dies ist ein gutes Thema für einen separaten Beitrag.

Wie Sie wahrscheinlich bereits vermutet haben, wird hier eine andere Funktion kompiliert, nämlich Test::<init> , ein Konstruktor der Test . Da der Code den Konstruktor (new Test() ) aufruft, wird er bei jedem Aufruf von f genau nach 5 Aufrufen gleichzeitig mit der Funktion f kompiliert.

Im Prinzip kann dies die Diskussion über Option (2), JIT-Kompilierung, beenden. Wie Sie sehen, wird die Funktion in diesem Fall zuerst von der JVM interpretiert und dann nach fünffacher Interpretation dynamisch kompiliert. Ich möchte das letzte Detail bezüglich der JIT-Kompilierung hinzufügen, nämlich die Option -XX:+PrintAssembly zu erwähnen. Wie der Name schon sagt, gibt es eine kompilierte Version der Funktion an die Konsole aus (kompilierte Version = nativer Maschinencode = Assembler-Code). Dies funktioniert jedoch nur, wenn sich im Bibliothekspfad ein Disassembler befindet. Ich denke, der Disassembler kann sich in verschiedenen JVMs unterscheiden, aber in diesem Fall handelt es sich um hsdis - einen Disassembler für openjdk. Der Quellcode der hsdis-Bibliothek oder ihrer Binärdatei kann an verschiedenen Stellen verwendet werden. In diesem Fall habe ich diese Datei kompiliert und hsdis-amd64.so in JAVA_HOME/lib/server .

Jetzt können wir diesen Befehl ausführen. Aber zuerst muss ich das hinzufügen, um -XX:+PrintAssembly müssen auch die -XX:+UnlockDiagnosticVMOptions hinzufügen, die vor der Option PrintAssembly folgen muss. Ist dies nicht der PrintAssembly , werden Sie von der JVM vor einer falschen Verwendung der Option PrintAssembly . Lassen Sie uns diesen Code ausführen:

 java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test 

Die Ausgabe wird lang sein und es wird Zeilen geben wie:

 0x00007f4b7cab1120: mov 0x8(%rsi),%r10d 0x00007f4b7cab1124: shl $0x3,%r10 0x00007f4b7cab1128: cmp %r10,%rax 

Wie Sie sehen können, werden die entsprechenden Funktionen in nativen Maschinencode kompiliert.

Besprechen Sie abschließend Option 3, AOT. Die Kompilierung vor der Ausführung, AOT, war vor Version 9 in Java nicht verfügbar.

In JDK 9 ist ein neues Tool erschienen, jaotc - wie der Name schon sagt, handelt es sich um einen AOT-Compiler für Java. Die Idee ist folgende: Führen Sie den Java-Compiler "javac", dann den AOT-Compiler für Java "jaotc" und anschließend die JVM "java" wie gewohnt aus. Die JVM führt normalerweise die Interpretation und JIT-Kompilierung durch. Wenn die Funktion jedoch über AOT-kompilierten Code verfügt, verwendet sie diesen direkt und greift nicht auf Interpretation oder JIT-Kompilierung zurück. Lassen Sie mich erklären: Sie müssen den AOT-Compiler nicht ausführen, er ist optional. Wenn Sie ihn verwenden, können Sie nur die gewünschten Klassen kompilieren, bevor er ausgeführt wird.

Erstellen wir eine Bibliothek, die aus einer AOT-kompilierten Version von Test::f . Vergessen Sie nicht: Um dies selbst zu tun, benötigen Sie JDK 9 in Build 150+.

 jaotc --output=libTest.so Test.class 

Als Ergebnis wird libTest.so generiert, eine Bibliothek, die AOT-kompilierten nativen Funktionscode enthält, der in der libTest.so . Sie können die in dieser Bibliothek definierten Zeichen anzeigen:

 nm libTest.so 

In unserer Schlussfolgerung wird es unter anderem Folgendes geben:

 0000000000002120 t Test.f()I 00000000000021a0 t Test.<init>()V 00000000000020a0 t Test.main([Ljava/lang/String;)V 

Alle unsere Funktionen, Konstruktor, f , statische Methode main sind also in der Bibliothek libTest.so .

Wie bei der entsprechenden Option "Java" kann die Option in diesem Fall von einer Datei begleitet werden. Hierzu gibt es die Option –compile-Befehle für jaotc. JEP 295 enthält relevante Beispiele, die ich hier nicht zeigen werde.

Lassen Sie uns nun "Java" ausführen und prüfen, ob AOT-kompilierte Methoden verwendet werden. Wenn Sie "Java" wie zuvor ausführen, wird die AOT-Bibliothek nicht verwendet, und dies ist nicht überraschend. Um diese neue Funktion nutzen zu können, steht die Option -XX:AOTLibrary zur Verfügung, die Sie angeben müssen:

 java -XX:AOTLibrary=./libTest.so Test 

Sie können mehrere AOT-Bibliotheken angeben, die durch Kommas getrennt sind.

Die Ausgabe dieses Befehls ist genau die gleiche wie beim Starten von "Java" ohne AOTLibrary , da sich das Verhalten des Testprogramms überhaupt nicht geändert hat. Um zu überprüfen, ob AOT-kompilierte Funktionen verwendet werden, können Sie eine weitere neue Option -XX:+PrintAOT .

 java -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test 

Vor der Ausgabe des Test zeigt dieser Befehl Folgendes an:

  9 1 loaded ./libTest.so aot library 99 1 aot[ 1] Test.main([Ljava/lang/String;)V 99 2 aot[ 1] Test.f()I 99 3 aot[ 1] Test.<init>()V 

Wie geplant wird die AOT-Bibliothek geladen und AOT-kompilierte Funktionen werden verwendet.

Wenn Sie interessiert sind, können Sie den folgenden Befehl ausführen und prüfen, ob die JIT-Kompilierung stattfindet.

 java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test 

Wie erwartet findet keine JIT-Kompilierung statt, da die Methoden in der Testklasse vor der Ausführung kompiliert und als Bibliothek bereitgestellt werden.

Eine mögliche Frage ist: Wenn wir einen nativen Funktionscode bereitstellen, wie stellt die JVM dann fest, ob der native Code veraltet / veraltet ist? Als letztes Beispiel ändern wir die Funktion f und setzen a auf 6.

 public int f() throws Exception { int a = 6; return a; } 

Ich habe dies nur getan, um die Klassendatei zu ändern. Jetzt lassen wir javac kompilieren und führen den gleichen Befehl wie oben aus.

 javac Test.java java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test 

Wie Sie sehen können, habe ich "jaotc" nicht nach "javac" ausgeführt, daher ist der Code aus der AOT-Bibliothek jetzt alt und falsch, und die Funktion f hat a = 5.

Die Ausgabe des obigen Befehls "Java" zeigt:

 228 56 b Test::<init> (5 bytes) 229 57 b Test::f (5 bytes) 

Dies bedeutet, dass die Funktionen in diesem Fall dynamisch kompiliert wurden, sodass der aus der AOT-Kompilierung resultierende Code nicht verwendet wurde. Daher wurde eine Änderung in der Klassendatei festgestellt. Wenn die Kompilierung mit javac durchgeführt wird, wird der Fingerabdruck in die Klasse eingegeben, und der Klassenfingerabdruck wird auch in der AOT-Bibliothek gespeichert. Da sich der neue Fingerabdruck der Klasse von dem in der AOT-Bibliothek gespeicherten unterscheidet, wurde im Voraus kompilierter nativer Code (AOT) nicht verwendet. Das ist alles, was ich Ihnen über die letzte Kompilierungsoption vor der Ausführung erzählen wollte.

In diesem Artikel habe ich versucht, anhand einfacher realistischer Beispiele zu erklären und zu veranschaulichen, wie die JVM Java-Code ausführt: Interpretieren, dynamisch kompilieren (JIT) oder im Voraus (AOT) - außerdem erschien die letzte Gelegenheit nur in JDK 9. Ich hoffe, Sie haben etwas gelernt neu.

Source: https://habr.com/ru/post/de466529/


All Articles