W3docs

Java JIT-Kompilierung

Wie der JIT-Compiler der JVM heißen Java-Bytecode zur Laufzeit in nativen Maschinencode optimiert.

Java ist bekannt für „einmal kompilieren, überall ausführen" – aber das ist nur die halbe Wahrheit. Der javac-Compiler wandelt Ihren Quellcode in Bytecode um, nicht in nativen Maschinencode, und die JVM beginnt damit, diesen Bytecode Schritt für Schritt zu interpretieren. Das Stück, das Java schnell macht, ist der JIT-Compiler (Just-In-Time): Während Ihr Programm läuft, beobachtet die JVM, welche Methoden am häufigsten aufgerufen werden, und kompiliert diese „heißen" Methoden im laufenden Betrieb in optimierten nativen Code.

Dieses Kapitel erklärt, wie das zweistufige Kompilierungsmodell funktioniert, was HotSpots Stufenkompilierer macht und warum ein Java-Programm umso schneller wird, je länger es läuft. Es baut darauf auf, wie die JVM Ihren Code lädt und ausführt – siehe JVM-Architektur und Ein Java-Programm kompilieren und ausführen für den größeren Zusammenhang.

Zwei Compiler, zwei Aufgaben

In der Java-Welt gibt es eigentlich zwei Compiler, und sie zu verwechseln ist ein häufiger Anfängerfehler.

CompilerWann er läuftEingabeAusgabe
javac (AOT)Zur Build-Zeit.java-QuellcodePortabler .class-Bytecode
JIT (HotSpot)Zur Laufzeit, innerhalb der JVMBytecodeNativer Maschinencode

javac läuft einmal und erzeugt plattformunabhängigen Bytecode. Der JIT lebt innerhalb der laufenden JVM und erzeugt CPU-spezifischen Maschinencode, der auf den genauen Prozessor zugeschnitten ist, auf dem Sie arbeiten. Deshalb läuft dieselbe .jar-Datei überall, kann aber dennoch nahezu native Geschwindigkeit erreichen.

// Build time: javac Hello.java  ->  Hello.class (bytecode)
// Run time:   java Hello        ->  JVM interprets, then JIT-compiles hot methods
public class Hello {
    public static void main(String[] args) {
        System.out.println("Bytecode now, native code soon.");
    }
}

Zuerst Interpreter, dann JIT

Wenn eine Methode zum ersten Mal ausgeführt wird, interpretiert die JVM sie: Es entstehen keine Kompilierungskosten, sodass der Start schnell ist, aber jeder Bytecode wird langsam ausgeführt. Die JVM führt pro Methode einen Aufrufzähler (und einen Back-Edge-Zähler für Schleifen). Sobald eine Methode oft genug aufgerufen wird, um einen Schwellenwert zu überschreiten, übergibt die JVM sie dem JIT zur Kompilierung in nativen Code, und zukünftige Aufrufe springen direkt in diese schnelle Version.

Deshalb wird ein lange laufender Server nach dem Warm-up schneller: Die Methoden auf seinem heißen Pfad werden schließlich kompiliert, während selten genutzter Code interpretiert bleibt (sodass kein Kompilierungsaufwand für ihn verschwendet wird).

// 'process' is on the hot path. After enough calls it gets JIT-compiled;
// 'logRareError' may stay interpreted forever because it almost never runs.
void handleRequest(Request r) {
    process(r);                 // hot: many invocations -> compiled
    if (r.isMalformed()) {
        logRareError(r);        // cold: rarely called -> stays interpreted
    }
}

Stufenkompilierung: C1 und C2

Modernes HotSpot verwendet Stufenkompilierung (Tiered Compilation), die zwei JIT-Compiler kombiniert, damit Sie schnellen Start und Spitzenleistung erhalten:

  • C1 (der Client-Compiler) kompiliert schnell mit leichter Optimierung. Er bringt heiße Methoden schnell zu nativem Code und fügt Profiling-Zähler ein.
  • C2 (der Server-Compiler) kompiliert langsamer, aber optimiert aggressiv und nutzt das von C1 gesammelte Profil (Inlining, Loop-Unrolling, Escape-Analyse, Dead-Code-Eliminierung).

Eine Methode steigt durch Stufen auf, je heißer sie wird:

StufeWas den Code ausführtKompromiss
Stufe 0InterpreterKeine Kompilierungskosten, langsamste Ausführung
Stufe 3C1 mit ProfilingSchnell zu erzeugen, moderate Geschwindigkeit, sammelt Daten
Stufe 4C2 vollständig optimiertLangsam zu erzeugen, schnellste Ausführung

Da C2 auf Basis des beobachteten Verhaltens optimiert, kann er Wetten eingehen, die der statische javac-Compiler nie hätte eingehen können – zum Beispiel das Inlining eines virtuellen Aufrufs, weil in der Praxis immer nur eine Implementierung auftaucht.

// C2 can speculatively inline this even though 'pay' is virtual,
// because profiling showed every call so far used CreditCard.
abstract class Payment { abstract void pay(int cents); }
class CreditCard extends Payment { void pay(int cents) { /* ... */ } }

void checkout(Payment p) {
    p.pay(1999);   // megamorphic in theory; monomorphic in practice -> inlined
}

Deoptimierung: eine Wette rückgängig machen

Spekulative Optimierungen können sich als falsch herausstellen. Wenn C2 CreditCard.pay ingelinnt hat und dann endlich ein PayPal-Objekt eintrifft, ist der optimierte Code nicht mehr gültig. HotSpot behandelt dies mit Deoptimierung: Es verwirft den fehlerhaften nativen Code, fällt für diese Methode auf den Interpreter zurück und kann sie später mit den neuen Informationen neu kompilieren. Dieses Sicherheitsnetz ermöglicht es dem JIT, aggressiv zu optimieren, ohne jemals falsche Ergebnisse zu produzieren.

// First 100000 calls: only CreditCard -> C2 inlines aggressively.
// Call 100001 passes a PayPal -> the assumption breaks ->
//   HotSpot deoptimizes, reverts to interpreter, and recompiles later.
checkout(new CreditCard());
checkout(new PayPal());   // triggers deoptimization of the inlined version

Die Stufen mit einem ausführbaren Beispiel beobachten

Ein echtes Warm-up-Benchmark benötigt Millionen von Schleifeniterationen, die eine Sandbox nicht ausführen kann. Stattdessen modelliert das folgende Programm die Beförderungsentscheidung, die HotSpot trifft – eine Methode nach der Anzahl ihrer Aufrufe im Vergleich zu den Standard-Stufenschwellenwerten klassifizieren – und liest echte JIT-Fakten aus der laufenden JVM über CompilationMXBean aus. Führen Sie es aus und beobachten Sie, wie eine Methode von interpretiert über C1 zu C2 aufsteigt, wenn ihre Aufrufanzahl steigt.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen kann:

  • Der JIT identifiziert sich als HotSpot 64-Bit Tiered Compilers (über CompilationMXBean.getName()), was bestätigt, dass C1 und C2 bei einem normalen java-Start auf einer HotSpot-JVM aktiv sind.
  • Methoden, die nur 1 oder 500 Mal aufgerufen werden, bleiben bei Stufe 0 (interpretiert) – der JIT verschwendet keinen Aufwand für kalten Code.
  • Das Überschreiten des 2000-Schwellenwerts befördert die Methode zu Stufe 3 (C1 kompiliert), der schnell erzeugten nativen Version, die auch profiliert.
  • Das Überschreiten von 10000 (und 100000) befördert sie zu Stufe 4 (C2), dem vollständig optimierten Code, der Spitzengeschwindigkeit liefert.
  • CompilationMXBean.getTotalCompilationTime() zeigt echte JIT-Aktivität von innerhalb Java, was beweist, dass die Kompilierung während das Programm läuft stattfindet, nicht im Voraus.

Den JIT selbst beobachten

Bei einem echten java-Start (außerhalb einer Sandbox) können Sie HotSpot mit Kommandozeilenflags in Echtzeit beim Kompilieren zusehen:

# Print each method as it is compiled, with its tier number in the second column.
java -XX:+PrintCompilation MyApp

# Dump a one-line summary of every compilation method HotSpot supports.
java -XX:+PrintFlagsFinal -version | grep -i tier

Einige praktische Erkenntnisse:

  • Wärmen Sie sich vor dem Benchmarking auf. Das Messen einer Methode bei ihrem ersten Aufruf misst den Interpreter, nicht den optimierten Code. Mikrobenchmarks sollten zuerst tausende von Iterationen durchlaufen (Tools wie JMH erledigen dies für Sie), damit C2 den heißen Pfad kompiliert hat.
  • Start- vs. Spitzengeschwindigkeit ist ein echter Kompromiss. Kurzlebige Programme (CLI-Tools, serverlose Funktionen) beenden sich möglicherweise, bevor C2 überhaupt aktiv wird, sodass sie hauptsächlich interpretiert oder C1-kompiliert laufen. Lange laufende Server erreichen nach dem Warm-up ihren Spitzendurchsatz.
  • Sie müssen die Schwellenwerte selten anpassen. Standardwerte funktionieren für die meisten Workloads gut. Die obigen Flags dienen dem Verständnis und der Diagnose, nicht dem alltäglichen Code.

JIT-Kompilierung und der Garbage Collector sind die beiden Laufzeitsysteme, die der JVM ihre Leistung verleihen; beide arbeiten automatisch, während Ihr Programm läuft.

Übung

Übung
Was löst in HotSpot Tiered Compilation aus, dass eine Methode vom Interpreter zu JIT-kompiliertem nativen Code befördert wird?
Was löst in HotSpot Tiered Compilation aus, dass eine Methode vom Interpreter zu JIT-kompiliertem nativen Code befördert wird?
Was this page helpful?