Java-Leistung Kapitel 4 Funktionsweise des JIT-Compilers

O'Reilly Japan \ -Java Performance Zusammenfassung von Kapitel 4 dieses Buches

Kapitel 1 Einführung \ -Qiita Kapitel 2 Ansatz für Leistungstests \ -Qiita Kapitel 3 Java Performance Toolbox \ -Qiita ← Vorheriger Artikel Kapitel 4 Funktionsweise des JIT-Compilers --Qiita ← Dieser Artikel Kapitel 5 Grundlagen der Speicherbereinigung \ -Qiita ← Nächster Artikel

4.1 Übersicht über den JIT-Compiler

Beispiele für Unterschiede zwischen Compiler- und Interpreter-Sprachen

Wenn Sie zwei Werte aus dem Hauptspeicher lesen und hinzufügen ... Ein guter Compiler führt eine Anweisung aus, die Daten liest, dann eine andere Anweisung ausführt und dann hinzufügt. Sie können nicht, weil der Interpreter jeweils nur eine Zeile sieht.

Dolmetscher haben den Vorteil der Portabilität

Die neuere Version der CPU kann fast alle Anweisungen der vorherigen Version der CPU ausführen, jedoch nicht umgekehrt (wie die AVX-Anweisungen des Intel Sandy Bridge-Prozessors). Es gibt eine Lösung wie die Durchführung der Verarbeitung, bei der die Leistung in der für jede CPU vorbereiteten gemeinsam genutzten Bibliothek wichtig ist.

Java befindet sich in einer Zwischenposition und wird nach dem Kompilieren in Java-Bytecode gleichzeitig mit der Ausführung auf jede Plattform kompiliert.

Hotspot-Kompilierung

Zum Beispiel, wenn Sie Code wie "b = obj1.equals (obj2)" ausführen möchten Um herauszufinden, welche equals () -Methode ausgeführt werden soll, schauen Sie sich den Typ (Klasse) von obj1 an. Wenn dieser Code ausgeführt wird, heißt "obj1" immer "java.lang.String # equals" Optimieren Sie, um die Methode direkt aufzurufen.

Register und Hauptspeicher

Wann ist der Compiler so optimiert, dass er aus dem Hauptspeicher in Registern speichert? Kann erwähnt werden

public class RegisterTest {
  private int sum;
  public void calculateSum(int n) {
    for (int i = 0; i < n; i++) {
      sum += i;
    }
  }
}

Für diesen Code vor dem Lesen der Summe aus dem Hauptspeicher Die Optimierung kann durchgeführt werden, indem die Summe in einem Register gehalten, wiederholt und das Berechnungsergebnis zur Summe im Hauptspeicher addiert wird.

Das von einem anderen Thread verwendete Register kann nicht gelesen werden (siehe Kapitel 9). Register werden besonders aggressiv verwendet, wenn die Escape-Analyse (am Ende dieses Kapitels beschrieben) aktiviert ist.

4.2 Grundlegende Optimierung (Client-Compiler und Server-Compiler)

Es gibt Client-Typ und Server-Typ. Es wird von den Befehlszeilenargumenten "-client" und "-server" so genannt.

In den meisten Fällen wird -XX nicht als Flag zur Angabe des Compilers verwendet. Die Ausnahme ist die gestufte Kompilierung. -XX:+TieredCompilation Für die hierarchische Kompilierung ist ein Server-Compiler erforderlich.

Der Client-Compiler beginnt früh mit dem Kompilieren, ist also in den frühen Phasen schnell. Andererseits braucht der Server-Compiler Zeit für die Optimierung.

Hierarchische Kompilierung ist eine Methode, bei der der Server-Compiler erneut kompiliert, wenn der Code "heiß" wird. Die hierarchische Kompilierung ist in Java 8 standardmäßig aktiviert.

Die hierarchische Kompilierung in Java 7 ist skurril und überschreitet beispielsweise schnell die Größe des Code-Cache der JVM.

4.3 Java- und JIT-Compilerversionen

Es gibt drei Versionen wie folgt

--32bit Client Compiler (-client) --32bit Server Compiler (-server) --64bit Server Compiler (-d64)

Wenn es sich um ein 32-Bit-Betriebssystem handelt, muss die JVM auch eine 32-Bit-Version sein. Mit einem 64-Bit-Betriebssystem können Sie entweder JVM verwenden.

Wenn der Heap 3 GB oder weniger beträgt, benötigt die 32-Bit-Version weniger Speicher und ist schneller. Es scheint, dass 32 Bit niedrigere Speicherreferenzkosten haben als 64 Bit.

In Kapitel 8 wird die Komprimierung gewöhnlicher Objektzeiger erläutert. Eine 32-Bit-Adresse kann auch mit einer 64-Bit-JVM verwendet werden. Da der bei der Ausführung verwendete native Code jedoch eine 64-Bit-Adresse verwendet, wird viel Speicher benötigt.

In Programmen, die 8-Byte-Typen (lang, doppelt) stark nutzen, ist die 32-Bit-JVM langsam, da die 64-Bit-Register der CPU nicht verwendet werden können.

32bitOS hat eine Wand von 4 GB (2 ^ 32)

↓ Zugehörige offizielle Materialien CompressedOops

↓ zeigt den Standard-Java-Compiler an

java -version

↓ Zugehörige offizielle Materialien java

4.4 Zwischenabstimmung

Code-Cache-Optimierung

Wenn die JVM den Code kompiliert, wird eine Reihe von Anweisungen in der Assemblersprache im Code-Cache gespeichert. Die Größe des Code-Cache ist festgelegt. Wenn es voll ist, kann es nicht mehr kompiliert werden und arbeitet mit dem Interpreter.

Bei der hierarchischen Kompilierung mit Java7 geht der Standard-Cache häufig der Standardgröße aus (einige SIer lassen mich die Java-Version immer noch nicht auf 8 oder höher erhöhen, und in diesem Fall habe ich solche Probleme ... )

Es gibt keine Möglichkeit zu wissen, wie viel Code-Cache Ihre Anwendung benötigt. Sie müssen ihn also tatsächlich ausführen und prüfen, ob er ausreicht (siehe unten, um zu überprüfen).

---XX: XX: InitialCodeCacheSize-N: Größe des anfänglichen Code-Cache. In meiner Umgebung wurden standardmäßig 2.555.904 Byte verwendet

---XX: XX: ReservedCodeCacheSize = N: Maximale Größe. In meiner Umgebung wurden standardmäßig 251.658.240 Byte verwendet

---XX: XX: CodeCacheExpansionSize = N: Erweiterte Größe des Code-Cache. In meiner Umgebung wurden standardmäßig 65.536 Byte verwendet

momose@momose-pc:~$ java -version
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu219.04.1)
OpenJDK 64-Bit Server VM (build 11.0.3+7-Ubuntu-1ubuntu219.04.1, mixed mode, sharing)
momose@momose-pc:~$ 

Was passiert, wenn ich eine große ReservedCodeCacheSize von 1 GByte spezifiziere, damit ich mir keine Sorgen machen muss, dass mir der Code-Cache ausgeht? Die JVM reserviert den nativen Speicherbereich für 1 GB. Es wird jedoch erst zugewiesen, wenn es verwendet wird.

↓ Referenzmaterialien [\ [tips ] \ [Java ] Überprüfen der Verwendung des CodeCache-Bereichs \ -Akiras technische Hinweise](http://luozengbin.github.io/blog/2015-09-01-%5Btips%5D%5Bjava% 5Dcodecache% E9% A0% 98% E5% 9F% 9F% E4% BD% BF% E7% 94% A8% E7% 8A% B6% E6% B3% 81% E3% 81% AE% E7% A2% BA% E8% AA% 8D% E6% 96% B9% E6% B3% 95.html)

Sie können jconsole verwenden, um die Größe Ihres Code-Cache zu überwachen. Wenn Sie im Speicherbereich Speicherpoolcode-Cache auswählen, wird ein Diagramm angezeigt (bis zu Java 8).

Es wird gesagt, dass Java 9 oder höher in einem Bereich namens Code Heap verwaltet wird. ↓ Registerkarte Speicher in jconsole CodeHeap.png

Laut -XX: + SegmentedCodeCache davon (https://docs.oracle.com/javase/jp/9/tools/java.htm), Es scheint, dass die Verarbeitung durch Teilen des Segments die Codefragmentierung verhindert und die Effizienz verbessert.

Gemäß Java9 \ (\ basierend auf Oracle JVM) Catchup \ -Qiita ist es je nach Codetyp in 3 Segmente unterteilt.

Schwellenwert kompilieren

Die Anzahl der Ausführungen wirkt sich auf die Kompilierung aus.

Es gibt nur einen Fall, in dem Sie den Kompilierungsschwellenwert anpassen sollten. Wenn die Summe der folgenden zwei Werte den Schwellenwert überschreitet, wird sie für den Compiler in die Warteschlange gestellt. Dies wird als "Standardzusammenstellung" bezeichnet (nicht der offizielle Name).

Mit Standardkompilierung, langer Verarbeitung in einer Schleife, langer Methode ist es nicht gut optimiert, Wenn der Hinterkantenzähler den Schwellenwert überschreitet, wird nur die Schleife kompiliert. Diese Zusammenstellung wird als "OSR (On-Stack-Ersatz)" bezeichnet.

Der Schwellenwert für die Standardkompilierung kann mit dem Flag -XX: CompileThreshold = N angegeben werden. Der Standardwert ist 1500 für den Client-Compiler und 10000 für den Server-Compiler.

OSR-Kompilierungsschwellenbedingung

Gegenkantenzählerwert> CompileThreshold(OnStackReplacePercentage 
 - InterpreterProfilePercentage) / 100

---XX: InterpreterProfilePercentage = N ist standardmäßig 33 ---XX: OnStackReplacePercentage = N ist standardmäßig 933 für den Client-Compiler und 140 für den Server-Compiler

Der Schwellenwert für den Client-Compiler ist also

1500 * (933 - 33) / 100 = 13500

Im Fall des Server-Compilers ist der Schwellenwert also

10000 * (140 - 33) / 100 = 10700

Wird sein

Jedes Mal, wenn die JVM einen sicheren Punkt erreicht, wird der Wert jedes Zählers dekrementiert. Daher werden irgendwann nicht alle Methoden kompiliert. Es gibt also einige "schleimige" Methoden, die ziemlich oft ausgeführt werden, aber nicht kompilieren (nicht heiß) (was auch einer der Gründe ist, warum die hierarchische Kompilierung so schnell ist).

Das Flag -XX: + PrintCompilation (Standardwert false). Jedes Mal, wenn Sie kompilieren

ID-Attribut für die Zeitstempel-Kompilierung(Hierarchische Kompilierungsebene)Größe des Methodennamens deoptimiert

Ein Protokoll mit dem Inhalt wie wird angezeigt.

Das Attribut ist

--% : OSR-Kompilierung --s: synchronisierte Methode --! : Methode hat Würfe -- b: Kompilieren im Blockierungsmodus (nicht im aktuellen Java ausgegeben) --n: Vom Compiler generierter nativer Methoden-Wrapper

Die Größe entspricht der Größe des Java-Bytecodes Wenn es deoptimiert ist, erhalten Sie eine Meldung, dass es deoptimiert wurde.

Sie können auch Informationen zum Kompilieren bereits laufender Java-Programme erhalten.

jstat -compiler ${Prozess ID}

Sie können die letzte kompilierte Version auch alle 1000 Millisekunden anzeigen.

jstat -printcompilation ${Prozess ID} 1000

Die OSR-Kompilierung ist oft zeitaufwändig.

4.5 Erweiterte Abstimmung

Maniac Content scheint für JVM-Ingenieure zu sein. Es ist unwahrscheinlich, dass Sie den hier beschriebenen Optimierungsinhalt implementieren ...

Compiler-Thread

Die Kompilierung wird asynchron ausgeführt, und die Anzahl der Threads des Compilers ändert sich abhängig von der Anzahl der CPUs und dem Typ des Compilers. Die Anzahl der Threads kann mit -XX: CICompilerCount = N geändert werden. Bei der hierarchischen Kompilierung ist 1/3 die Clientkompilierung und der Rest die Serverkompilierung.

Wenn Sie -XX: + BackgroundCompilation angeben, wird verhindert, dass die Kompilierung asynchron ist.

In der Reihe

Code, auf den über Getter / Setter zugegriffen wird, wird von modernen Compilern eingebunden. Inlining ist standardmäßig aktiviert. Es kann auch mit -XX: -Inline deaktiviert werden.

Die Bedingungen für das Inlining sind Hotness und Bytecode-Größe.

Wenn es heiß ist und die Bytecodegröße 325 Bytes (änderbar mit -XX: MaxFreqInlineSize = N) oder weniger beträgt, wird Inlining ausgeführt. Wenn die Größe 35 Byte oder weniger beträgt (änderbar mit -XX: MaxInlineSize = N), wird das Inlining unbedingt ausgeführt.

Fluchtanalyse

Optimierung, wenn -XX: + DoEscapeAnalysis (Standardwert ist true) aktiviert ist. Es scheint verschiedene Dinge zu tun, aber zum Beispiel

public class Factorial {
    private BigInteger factorial;
    private int n;
    public Factorial(int n) {
        this.n = n;
    }
    public synchronized BigInteger getFactorial() {
        if (factorial == null)
            factorial = ...;
        return factorial;
    }
}

Gegen

ArrayList<BigInteger> list = new ArrayList<BigInteger>();
for (int i = 0; i < 100; i++) {
    Factorial factorial = new Factorial(i);
    list.add(factorial.getFactorial());
}

Erweiterte Optimierung wird durchgeführt (in seltenen Fällen kann ein Fehler vorliegen)

Deoptimiert

Die Nichtoptimierung ist kein Teilnehmer mehr, sondern wird in einen Zombie verwandelt und der GC wird ausgeführt

Wenn es kein Teilnehmer mehr ist

Es gibt zwei, und die erste besteht darin, eine Optimierung durchzuführen, indem sie der Implementierungsklasse für eine bestimmte Schnittstelle zugeordnet wird. Wenn diese Prämisse jedoch fehlerhaft ist, wird sie nicht optimiert. Die zweite ist die Implementierung der hierarchischen Kompilierung, die deoptimiert wird, indem sie nach Abschluss der Kompilierung durch den Server-Compiler als kein Teilnehmer markiert wird.

Zombie-Code

Wenn der erstellte Zombie im Kompilierungsprotokoll angezeigt wird, wird nicht eingegebener Code abgebrochen und GC ausgeführt.

↓ Referenzmaterialien Java-JA13-Architect-evans.pdf

4.7 Ebene der hierarchischen Kompilierung

--0: Vom Interpreter ausgeführter Code --1: Vom Client-Compiler im einfachen Modus kompilierter Code --2: Code, der vom Client-Compiler im eingeschränkten Modus kompiliert wurde ―― 3: Vom Client-Compiler im Vollmodus kompilierter Code --4: Vom Server-Compiler kompilierter Code

Es funktioniert im Anfangszustand auf Stufe 0, und in den meisten Fällen scheint es zuerst auf Stufe 3 und dann auf Stufe 4 kompiliert zu werden. Level 1 und Level 2 werden verwendet, wenn die Compiler-Warteschlange voll ist (sie wird mit hoher Geschwindigkeit kompiliert, da kein Profiler verwendet wird). Natürlich geht der nicht optimierte Code auf Stufe 0 zurück.

4.8 Zusammenfassung

Außerdem scheint der letzte Modifikator ** keinen Einfluss auf die Leistung zu haben **.

Recommended Posts

Java-Leistung Kapitel 4 Funktionsweise des JIT-Compilers
Funktionsweise des JVM JIT-Compilers
Java-Leistung Kapitel 1 Einführung
Java Performance Kapitel 3 Java Performance Toolbox
Java-Leistung Kapitel 2 Ansatz für Leistungstests
[Java] Wie Spring DI funktioniert
Java-Leistung Kapitel 5 Grundlagen der Garbage Collection
[Java] Verwendung der File-Klasse
[Java] Verwendung der toString () -Methode
Studieren der Verwendung des Konstruktors (Java)
[Verarbeitung × Java] Verwendung der Schleife
[Java] So stellen Sie die Datums- und Uhrzeit auf 00:00:00 ein
[Java] So erhalten Sie das aktuelle Verzeichnis
[Verarbeitung × Java] Verwendung der Klasse
So erhalten Sie das Datum mit Java
[Verarbeitung × Java] Verwendung der Funktion
[Java] Verwendung der Calendar-Klasse
[Hinweis] Java: Erstellen Sie ein einfaches Projekt, während Sie lernen, wie die Einstellungsdatei funktioniert.
Tauchen Sie ein in die Funktionsweise von HashMap in Java
[Java] (für MacOS) Methode zur Einstellung des Klassenpfads
Verwendung der replace () -Methode (Java Silver)
[Java] So erhalten Sie die endgültige umgeleitete URL
[Java] Memo zum Schreiben der Quelle
Effektives Java Kapitel 2
Effektives Java Kapitel 6 34-35
Effektives Java Kapitel 4 15-22
Effektives Java Kapitel 3
Wie jul-to-slf4j funktioniert
[Java] So erhalten Sie die URL der Übergangsquelle
[Java] So lassen Sie den privaten Konstruktor in Lombok weg
Wie schreibe ich Scala aus der Perspektive von Java
[Java] So extrahieren Sie den Dateinamen aus dem Pfad
[Java] So erhalten Sie den Maximalwert von HashMap
[Android Studio] [Java] So fixieren Sie den Bildschirm vertikal