Codeoptimierung durch JIT-Compiler

Wenn eine Methode zur Kompilierung ausgewählt wird, führt die JVM ihre Bytecodes dem JIT-Compiler (Just-in-time-Compiler) zu. Der JIT-Compiler muss die Semantik und Syntax der Bytecodes verstehen, um die Methode ordnungsgemäß kompilieren zu können.

Um dem JIT-Compiler bei der Analyse der Methode zu helfen, werden die Bytecodes zunächst in eine interne Darstellung namens Bäume umformuliert, die eher an Maschinencode als an Bytecodes erinnert. Analyse und Optimierungen werden dann an den Bäumen der Methode ausgeführt. Zum Schluss werden die Bäume in nativen Code übersetzt. Nachfolgend erhalten Sie einen kurzen Überblick über die Phasen der JIT-Kompilierung. Weitere Informationen finden Sie unter JIT-oder AOT-Problem diagnostizieren.

Der JIT-Compiler kann mehr als einen Kompilierungsthread verwenden, um JIT-Kompilierungstasks durchzuführen. Durch die Verwendung mehrerer Threads können Java-Anwendungen möglicherweise schneller gestartet werden. In der Praxis erzielen mehrere JIT-Kompilierungsthreads nur dann Leistungsverbesserungen, wenn es nicht verwendete Verarbeitungskerne im System gibt.

Die Standardanzahl von Kompilierungsthreads wird von der JVM ermittelt und hängt von der Systemkonfiguration ab. Wenn die sich ergebende Anzahl Threads nicht optimal ist, können Sie die JVM-Entscheidung mithilfe der Option -XcompilationThreads überschreiben. Informationen zur Verwendung dieser Option finden Sie unter -X Optionen.
Hinweis: Wenn Ihr System keine nicht verwendeten Verarbeitungskerne hat, ist es unwahrscheinlich, dass eine Erhöhung der Anzahl der Kompilierungsthreads zu einer Leistungsverbesserung führt.

Die Kompilierung teilt sich in folgende Phasen auf. Alle Phasen, ausgenommen der Generierung von nativem Code, beziehen sich auf plattformübergreifenden Code.

Phase 1 – Inlining

Das Inlining ist der Vorgang, bei dem die Bäume kleinerer Methoden in die Bäume ihrer Aufrufer aufgenommen werden. Hierdurch werden häufig ausgeführte Methodenaufrufe beschleunigt. Abhängig von der aktuellen Optimierungsstufe werden zwei Inliningalgorithmen mit unterschiedlichen Aggressivitätsstufen verwendet. Die folgenden Optimierungen werden in dieser Phase ausgeführt:
  • Trivial Inlining
  • Call Graph Inlining
  • Tail Recursion Elimination
  • Virtual Call Guard-Optimierungen

Phase 2 – Lokale Optimierungen

Lokale Optimierungen analysieren und verbessern jeweils einen kleinen Codeabschnitt. Viele lokale Optimierungen implementieren bewährte Verfahren, wie sie in klassischen statischen Compilern zum Einsatz kommen. Die folgenden Optimierungen sind verfügbar:
  • Lokale Datenflussanalysen und -optimierungen
  • Optimierung der Registernutzung
  • Vereinfachungen von Java-Idiomen
Diese Verfahren werden wiederholt angewendet, vor allem nach globalen Optimierungen, die möglicherweise weitere Verbesserungsmöglichkeiten verfügbar gemacht haben.

Phase 3 – Steuerungsablaufoptimierungen

Steuerungsablaufoptimierungen analysieren den Ablauf der Steuerung in einer Methode (oder bestimmten Teilen davon) und ordnen die Codepfade neu an, um ihre Effizienz zu verbessern. Die folgenden Optimierungen sind verfügbar:
  • Code umstellen, teilen und entfernen
  • Loop-Reduction und Loop-Inversion
  • Loop-Striding und Verschieben von schleifeninvariantem Code
  • Loop-Unrolling und Loop-Peeling
  • Loop-Versioning und Loop-Specialization
  • Durch Ausnahmebedingungen gesteuerte Optimierung
  • Switch-Analyse

Phase 4 – Globale Optimierungen

Globale Optimierungen bearbeiten die gesamte Methode auf einmal. Sie sind aufwändiger, d. h., sie benötigen mehr Kompilierzeit, können jedoch eine erhebliche Leistungszunahme bewirken. Die folgenden Optimierungen sind verfügbar:
  • Globale Datenflussanalysen und -optimierungen
  • Partieller Redundanzausschluss
  • Escape-Analysen
  • GC- und Speicherzuordnungsoptimierungen
  • Synchronisationsoptimierungen

Phase 5 – Generierung von nativem Code

Die Prozesse zur Generierung von nativem Code variieren je nach Plattformarchitektur. Im Allgemeinen werden während dieser Kompilierungsphase die Bäume einer Methode in Maschinencodeanweisungen übersetzt und einige kleinere Optimierungen werden entsprechend den Architekturmerkmalen vorgenommen. Der kompilierte Code wird in einem Teil des JVM-Prozessraums abgelegt, der als Code-Cache bezeichnet wird. Die Position der Methode wird aufgezeichnet, sodass zukünftige Aufrufe der Methode den kompilierten Code aufrufen. Zu jedem beliebigen Zeitpunkt besteht der JVM-Prozess aus den ausführbaren JVM-Dateien und einem Satz JIT-kompilierten Codes, der dynamisch mit dem Bytecode-Interpreter in der JVM verknüpft ist.