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.
| Subsystem | Aufgabe |
|---|---|
| Class Loader | Findet, lädt, verknüpft und initialisiert .class-Dateien in die Laufzeitumgebung |
| Laufzeitdatenbereiche | Der von der JVM verwaltete Speicher: Heap, Stacks, Methodenbereich, PC-Register |
| Ausführungsengine | Interpretiert 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: nullDie 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.
Was der Programmablauf zeigt:
- Der
RuntimeMXBeanbenennt die Ausführungsengine — eine HotSpot-Familie-VM (die Zeilevm namezeigt 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 einemnull-Elternteil — dem Bootstrap-Loader, der nativer Code ohne Java-Objekt ist. Die Hierarchie ist real und beobachtbar, keine Metapher. String.class.getClassLoader()istnull— Kerntypen ausjava.basekommen von demselben Bootstrap-Loader an der Spitze der Kette, weshalb Anwendungscode niemals sein eigenesStringeinsetzen 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)gibt5zurü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 einemStackOverflowErrorendet statt den Heap zu beschädigen.