[JAVA] Funktionsweise des JVM JIT-Compilers

Ich werde zusammenfassen, was ich über den Mechanismus des JIT-Compilers der JVM gelernt habe.

Umgebung

~/workspace/$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)

Was ist JIT Comparite?

Just In Time-Compiler, der auf JVM implementiert ist. Just In Time, ein Compiler, der kompiliert, "was Sie brauchen, wann Sie es brauchen".

Fluss bis zur Ausführung auf JVM

Grob gesagt fließt der folgende

Quellcode->kompilieren->Zwischencode->Ausführen (kompilieren)

image.png

Quelle

Die JVM-Sprache kompiliert nicht den gesamten interessierenden Quellcode, wenn Sie die Kompilierung ausführen (javac für Java, sbt compile für scala usw.). Erstellen Sie zunächst einen Zwischencode. (Java-Byte-Code). Durch Sandwiching des Prozesses zum Generieren dieses Zwischencodes kann derselbe Code auf jedem Betriebssystem ausgeführt werden, solange eine JVM-Umgebung vorhanden ist. (JVM muss für jedes Betriebssystem geeignet sein)

Danach wird der erstellte Zwischencode nicht sofort kompiliert (in nativen Code konvertiert). Der Interpreter kompiliert den Quellcode bei jeder Ausführung. (Java-Interpreter in der obigen Abbildung)

Dafür gibt es zwei Gründe.

1. Das Kompilieren verschwendet Kompilierungszeit, wenn der Code nur einmal verwendet wird

Da das Kompilieren für Code, der nur einmal aufgerufen wird, einige Zeit in Anspruch nimmt, verkürzt die Ausführung mit dem Interpreter die Gesamtzeit für die Ausführung. Andererseits kann kompilierter Code für häufig aufgerufenen Code schneller ausgeführt werden und sollte kompiliert werden. Die Erörterung des Schwellenwerts in der JVM, ob mit dem Interpreter ausgeführt oder kompiliert und ausgeführt werden soll, wird später beschrieben.

2. Sie können die beim Kompilieren verfügbaren Informationen sammeln.

Sie können die Informationen erhalten, die Sie kompilieren müssen, wenn Sie sie im Interpreter ausführen. Unter Verwendung der erfassten Informationen können zum Zeitpunkt der Kompilierung verschiedene Optimierungen durchgeführt werden. Diese Optimierung macht auch innerhalb dieses kompilierten Codes einen Unterschied in der Ausführungszeit.

Betrachten Sie zum Beispiel die equals () -Methode

Ich habe den folgenden Code.

test.scala


val b = obj1.equals(obj2)

Wenn der Interpreter die Methode "equals ()" erreicht, muss gesucht werden, ob die Methode "equals ()" die in obj1 definierte Methode oder die Methode des String-Objekts ist. Mit nur dem Interpreter wäre es Zeitverschwendung, jedes Mal zu suchen, wenn die Methode "equals ()" erreicht wird.

Wenn der Interpreter feststellt, dass obj1 eine Methode eines String-Objekts ist, kompiliert er die Methode equals () als Methode des String-Objekts. Es kompiliert und eliminiert die Zeit, die es beim Interpretieren für das Erkunden benötigt, was zu einem schnelleren Code führt.

Wie Sie sehen können, kompiliert der JIT-Compiler den Code nicht sofort, da er nicht optimiert werden kann, ohne den Code auszuführen und zu betrachten.

Drei Arten von JIT-Compilern

Es gibt drei Arten von JIT-Compilern. Ab Java8 ist der dritte hierarchische Compiler standardmäßig festgelegt.

Client-Compiler (C1)

Frühzeitig kompilieren

Server-Compiler (C2)

Sammeln Sie Informationen zum Verhalten Ihres Codes, bevor Sie kompilieren. Wie oben erwähnt, ist es optimiert und kompiliert, sodass es schneller als der Client-Compiler ist.

Hierarchischer Compiler

Eine Sammlung von Client-Compilern und Server-Compilern. In der Anfangsphase wird es in C1 kompiliert, und wenn die Informationen zur Optimierung gesammelt werden (wenn der Code heiß wird), wird es in C2 kompiliert.

Bestätigung

Sie können den konfigurierten Compiler mit Java-Version überprüfen In meinem Fall

~/workspace/$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)

Jeder Compiler sollte abhängig von der zu erstellenden Anwendung ordnungsgemäß verwendet werden. Wenn Sie beispielsweise eine GUI-Anwendung auf einer JVM ausführen, sollten Sie einen Client-Compiler verwenden, da es für UX besser ist, eine schnellere anfängliche Zugriffszeit zu haben, als die Verarbeitungsgeschwindigkeit zu erhöhen, wenn Sie sie verwenden.

Wählen Sie den Compiler entsprechend der Anwendung und dem, was Sie realisieren möchten.

Kompilierter Schwellenwert

Wie oben erwähnt, wird der Bytecode zuerst vom Interpreter ausgeführt. Zu welchem Zeitpunkt können Sie dann vom Interpreter zum JIT-Compiler wechseln? Es gibt zwei Schwellenwerte

Anrufzähler (Standardzusammenstellung)

Häufigkeit, mit der die Zielmethode aufgerufen wurde

Wenn diese Anzahl den Schwellenwert überschreitet, wird die Zielmethode zur Kompilierung in die Warteschlange gestellt und kompiliert.

Taschenrandzähler

Die Häufigkeit, mit der die Verarbeitung vom Code in der Schleife innerhalb der Methode zurückgegeben wird

Wenn diese Zahl den Schwellenwert überschreitet, wird die Schleife selbst zum Ziel der Kompilierung und wird kompiliert. Die zu diesem Zeitpunkt durchgeführte Kompilierung wird als OSR-Kompilierung bezeichnet. Wenn die Kompilierung abgeschlossen ist, wird sie gegen den auf dem Stapel kompilierten Code ausgetauscht, und der kompilierte Code wird ab dem nächsten Prozess ausgeführt.

Tuning

Die oben genannten Zählerschwellenwerte unterscheiden sich zwischen dem Client-Compiler und dem Server-Compiler. Sie müssen diesen Schwellenwert richtig einstellen.

Beispiel

Wenn Sie den Schwellenwert für die Standardkompilierung mit dem Server-Compiler senken, werden die für die Kompilierung erforderlichen Informationen reduziert, was die Optimierung erschwert und zu einem langsamen Kompilierungscode führt.

Dennoch ist es sinnvoll, die Schwelle zu senken

1. Die Aufwärmzeit kann etwas verkürzt werden

Korrekt.

2. Code, der nicht bei hohen Schwellenwerten kompiliert wird, wird ebenfalls kompiliert

In dieser Hinsicht ist es wahrscheinlich, dass der Schwellenwert für den Anrufzähler / Beutelkantenzähler schließlich erreicht wird, wenn der Code weiter ausgeführt wird. Der Zählerwert wird jedoch über die Zeit abgezogen.

Wie oben erwähnt, müssen Sie richtig einstellen.

Eigentlich stimmen

Beobachten und optimieren Sie das Verhalten des JIT-Compilers mit dem folgenden Code

Sie können die Option jvm in .jvmopts wie unten gezeigt angeben.

.jvmopts


-XX:+PrintCompilation
-XX:CompileThreshold=1

Es spuckt das Kompilierungsprotokoll wie unten gezeigt aus

Das Format ist

Zeitstempel-Kompilierung ID Attribut Methode Name Größe Deoptimierung


$ sbt run
41    1       3       java.lang.Object::<init> (1 bytes)
42    2       3       java.lang.String::hashCode (55 bytes)
44    3       3       java.lang.String::charAt (29 bytes)
45    4       3       java.lang.String::equals (81 bytes)
45    5     n 0       java.lang.System::arraycopy (native)   (static)
45    6       3       java.lang.Math::min (11 bytes)
45    7       3       java.lang.String::length (6 bytes)
52    8       1       java.lang.Object::<init> (1 bytes)
52    1       3       java.lang.Object::<init> (1 bytes)   made not entrant
53    9       3       java.util.jar.Attributes$Name::isValid (32 bytes)
53   10       3       java.util.jar.Attributes$Name::isAlpha (30 bytes)
・ ・ ・ ・

Sie können angeben, wie oft die Methodenschleife ausgeführt wird, bevor sie kompiliert wird.

Versuchen Sie es mit dem folgenden Code

Test.scala


object Test extends App{
  def compileTest() = {
    for (i <- 0 to 1000) {
      sampleLoop(i)
    }
  }

  def sampleLoop(num: Int) = {
    println(s"loopppp${num}")
  }

  println(compileTest())
}

.jvmopts


-XX:+PrintCompilation
-XX:CompileThreshold=1

Ergebnis

Da ich "-XX: CompileThreshold = 1" gesetzt habe, kann ich bestätigen, dass die "compileTest" -Methode kompiliert wurde, indem ich diesen Code einmal ausführe. Die sampleLoop -Methode ist ebenfalls eine Schleife, daher wird sie kompiliert.

9983 9336       3       Test$$$Lambda$3666/873055587::apply$mcVI$sp (5 bytes)
9983 9338       3       Test$::sampleLoop (1 bytes)
9983 9337       3       Test$::$anonfun$compileTest$1 (8 bytes)
9984 9334       4       java.lang.invoke.MethodType::makeImpl (66 bytes)
9986 9339   !   3       scala.Enumeration$Val::toString (55 bytes)
・ ・ ・

Die compileTest -Methode wird 9 Sekunden nach dem Start der JVM kompiliert.

Was ist zum Beispiel mit den folgenden Einstellungen?


object Test extends App{
  def compileTests() = {
    for (i <- 0 to 10) { //Wechseln Sie zu 10 Schleifen
      sampleLoop(i)
    }
  }

  def sampleLoop(num: Int) = {
    println(s"loopppp${num}")
  }

  println(compileTests())
}

.jvmopts


-XX:+PrintCompilation
-XX:CompileThreshold=100 #Ändern Sie den Schwellenwert auf 100 Mal

Wenn Sie "-XX: CompileThreshold = 100" usw. festlegen, wird die "compileTest" -Methode nicht nur durch einmaliges Ausführen des obigen Codes kompiliert. Außerdem wird die sampleLoop -Methode nicht ausgeführt, da sie nicht 100 Mal ausgeführt wird.

Zusammenfassung

Es ist leicht zu verstehen, wenn Sie sich den JIT-Kompilierungsprozess ansehen.

Referenz

Recommended Posts

Funktionsweise des JVM JIT-Compilers
Java-Leistung Kapitel 4 Funktionsweise des JIT-Compilers
Wie jul-to-slf4j funktioniert
[Hinweis] Über das Fizz_Buzz-Problem (Funktionsweise von Ruby on Rails)
Lassen Sie die JVM die Nummer lösen
Scala läuft auf einer JVM
[Java] Wie Spring DI funktioniert