W3docs

Java JVM-Architektur

Aufbau der JVM: Class Loader, Laufzeitdatenbereiche, Ausführungsengine und natives Interface.

Die Java Virtual Machine (JVM) ist das Programm, das Ihr Programm ausführt. Sie kompilieren .java-Quellcode in plattformneutralen .class-Bytecode, und die JVM lädt diesen Bytecode, verifiziert ihn, legt die Objekte im Speicher an und führt ihn auf realer Hardware aus. „Write once, run anywhere" bedeutet eigentlich: „Einmal kompilieren und die JVM jeder Plattform den Rest erledigen lassen." Dieses Kapitel beschreibt die interne Struktur der JVM — die drei Subsysteme, die jede Implementierung teilt — damit die folgenden Kapitel zum Speichermodell und zur Garbage Collection einen gemeinsamen Rahmen haben.

Drei Subsysteme

Eine JVM — unabhängig vom Hersteller — ist in drei zusammenarbeitende Subsysteme gegliedert. Alles andere ist Detail innerhalb eines dieser Subsysteme.

SubsystemAufgabe
Class LoaderFindet, lädt, verknüpft und initialisiert .class-Dateien in die Laufzeitumgebung
LaufzeitdatenbereicheDer von der JVM verwaltete Speicher: Heap, Stacks, Methodenbereich, PC-Register
AusführungsengineInterpretiert und JIT-kompiliert Bytecode und führt den Garbage Collector aus

Der Class Loader bringt Typen hinein, die Laufzeitdatenbereiche halten den Zustand, und die Ausführungsengine führt den Code aus. Ein Methodenaufruf berührt alle drei: Die zugehörige Klasse wird geladen, ihr Frame auf einen Stack gelegt, und ihr Bytecode wird ausgeführt.

Das Class-Loader-Subsystem

Klassen werden nicht alle beim Start geladen. Die JVM lädt eine Klasse verzögert (lazy), beim ersten Verweis, in drei Phasen: Loading (Bytes lesen), Linking (Bytecode verifizieren, statische Felder vorbereiten, Referenzen auflösen) und Initialization (statische Initialisierer und static { }-Blöcke ausführen).

Loader bilden eine Parent-First-Hierarchie. Wenn ein Loader nach einer Klasse gefragt wird, delegiert er aufwärts zum übergeordneten Loader, bevor er selbst sucht — so kommt ein Kerntyp wie String immer vom vertrauenswürdigen Bootstrap-Loader und kann niemals durch Anwendungscode überschattet werden.

// Walk the loader chain of any class
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
  System.out.println(loader.getName());
  loader = loader.getParent();
}
// A null result means the bootstrap loader (native, no Java object).
System.out.println(String.class.getClassLoader()); // prints: null

Die drei Standard-Loader, vom Kind zum Elternteil, sind der Application-(Classpath-)Loader, der Platform-Loader (JDK-Module wie java.sql) und der Bootstrap-Loader (Kern java.base, in nativem Code implementiert, als null dargestellt). Das Kapitel über Class Loading behandelt diesen Lebenszyklus und das Delegationsmodell vollständig; Module erklären, wie java.base und ähnliche paketiert sind.

Laufzeitdatenbereiche

Sobald eine Klasse geladen ist, leben ihr Code und ihre Daten in Bereichen, die die JVM für bestimmte Zwecke aufteilt:

  • Heap — thread-übergreifend geteilt; jedes Objekt und jedes Array lebt hier. Dies ist das, was der Garbage Collector verwaltet.
  • JVM Stacks — einer pro Thread. Jeder Methodenaufruf legt einen Frame mit lokalen Variablen und Operanden an; der Frame wird bei der Rückkehr entfernt. Tiefe Rekursion führt zu einem Überlauf (StackOverflowError).
  • Methodenbereich (Metaspace in modernen JVMs) — Klassen-Metadaten, der Laufzeit-Konstantenpool und statische Felder. Kein Heap-Speicher.
  • PC-Register — pro Thread; die Adresse der aktuell ausgeführten Bytecode-Anweisung.
// Heap allocation: 'new' carves space out of the heap
byte[] buffer = new byte[1024]; // lives on the heap, GC-managed

// Stack growth: each call adds a frame to this thread's stack
static long factorial(long n) {
  return n <= 1 ? 1 : n * factorial(n - 1); // each call = one more frame
}

Heap versus Nicht-Heap ist die entscheidende Trennung: Objektinstanzen befinden sich auf dem Heap; Klassenstrukturen und die Maschinerie selbst nicht. Das Kapitel Stack vs. Heap vergleicht diese beiden Bereiche im Detail.

Die Ausführungsengine

Die Ausführungsengine verwandelt Bytecode in Aktion. Moderne HotSpot-JVMs sind adaptiv: Sie beginnen mit der Interpretation von Bytecode (schneller Start), erstellen ein Profil der häufig aufgerufenen Methoden und übergeben diese dann dem Just-In-Time (JIT)-Compiler, der optimierten nativen Maschinencode erzeugt. Selten genutzter Code bleibt interpretiert; häufig genutzter Code wird kompiliert — Optimierungskosten entstehen nur dort, wo sie sich auszahlen.

Die Engine beherbergt auch den Garbage Collector, der Heap-Objekte zurückgewinnt, die von keiner lebendigen Referenz mehr erreichbar sind, sowie das Java Native Interface (JNI), die Brücke zu in C/C++ geschriebenen Bibliotheken.

// A hot loop: the JIT will compile sum() to native code after enough calls
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
  total += i; // interpreted at first, then JIT-compiled, then much faster
}

Ein praktisches Beispiel: Die laufende JVM inspizieren

Dieses Programm konfiguriert nichts — es fragt die laufende JVM, sich selbst über die java.lang.management-Beans und die Class-Loader-API zu beschreiben. Es benennt die VM und die Ausführungsengine, durchläuft die Loader-Hierarchie, meldet Heap- und Nicht-Heap-Speicher und lässt den Stack durch Rekursion wachsen.

java— editable, runs on the server

Was der Programmablauf zeigt:

  • Der RuntimeMXBean benennt die Ausführungsengine — eine HotSpot-Familie-VM (die Zeile vm name zeigt etwas wie 'OpenJDK 64-Bit Server VM') — und bestätigt, dass die JIT-fähige „Server"-Engine Ihren Bytecode ausführt, kein bloßer Interpreter.
  • Die Loader-Kette gibt etwas wie <unnamed> -> app -> platform -> bootstrap(null) aus: jeder Schleifenschritt stieg zum übergeordneten Loader hoch, und die Kette endete bei einem null-Elternteil — dem Bootstrap-Loader, der nativer Code ohne Java-Objekt ist. Die Hierarchie ist real und beobachtbar, keine Metapher.
  • String.class.getClassLoader() ist null — Kerntypen aus java.base kommen von demselben Bootstrap-Loader an der Spitze der Kette, weshalb Anwendungscode niemals sein eigenes String einsetzen kann. Die Parent-First-Delegation erfüllt ihren Zweck.
  • Die Speicherzeilen zeigen den Heap-Verbrauch in KB, aber einen Heap-Maximalwert in MB, und einen separaten Nicht-Heap-Wert: Objektinstanzen leben auf dem GC-verwalteten Heap, während Klassen-Metadaten (Metaspace) als Nicht-Heap gezählt werden — die beiden Bereiche werden unabhängig voneinander verfolgt.
  • depth(1) gibt 5 zurück, weil jeder rekursive Aufruf seinen eigenen Frame auf den JVM-Stack dieses Threads gelegt und beim Rückgabe entfernt hat; der Stack ist thread-spezifisch und frame-strukturiert, weshalb unkontrollierte Rekursion in einem StackOverflowError endet statt den Heap zu beschädigen.

Übungen

Übung
Ein Java-Programm referenziert die Klasse 'java.lang.String'. Welcher Loader stellt sie in der standardmäßigen Parent-First-Klassenlader-Hierarchie bereit, und wie erscheint dieser Loader im Java-Code?
Ein Java-Programm referenziert die Klasse 'java.lang.String'. Welcher Loader stellt sie in der standardmäßigen Parent-First-Klassenlader-Hierarchie bereit, und wie erscheint dieser Loader im Java-Code?
Was this page helpful?