Java Class Loading
Wie die JVM Klassen mit Class Loadern findet und lädt — Bootstrap, Platform, System und benutzerdefinierte Loader.
Bevor die JVM eine einzige Zeile Ihres Codes ausführen kann, muss sie die .class-Datei finden, ihren Bytecode lesen, verifizieren und in ein lebendes Class-Objekt im Speicher umwandeln. Diese Aufgabe gehört einem Class Loader. Class Loading macht java.lang.String verfügbar, ohne dass Sie etwas tun müssen, lässt ein JAR auf dem Classpath zur Laufzeit erscheinen und betreibt Plugin-Systeme, Applikationsserver und Hot-Reload-Tools. Dieses Kapitel zeigt, wie die Loader organisiert sind, wie die Delegation funktioniert und warum die Identität einer Klasse mehr als nur ihr Name ist.
Die Class-Loader-Hierarchie
Loader sind als eine Kette von Eltern angeordnet, jeder zuständig für eine andere Quelle von Klassen. In einem modernen JDK (9+) gibt es drei eingebaute Loader:
| Loader | Lädt | Gemeldet als |
|---|---|---|
| Bootstrap | Kern-JDK-Klassen (java.*, javax.* Basismodule) | null |
| Platform | Die restlichen JDK-Plattformmodule | ein PlatformClassLoader |
| System / Application | Ihr Code vom Classpath/Modulpfad | ein AppClassLoader |
Jede Klasse merkt sich den Loader, der sie definiert hat. Sie können jede Klasse fragen, welcher Loader sie erzeugt hat:
ClassLoader appLoader = MyApp.class.getClassLoader(); // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader(); // null = bootstrap
ClassLoader parent = appLoader.getParent(); // PlatformClassLoaderDer Bootstrap-Loader ist in nativem Code geschrieben, nicht in Java, weshalb String.class.getClassLoader() null zurückgibt anstatt ein Objekt — es gibt keine Java-ClassLoader-Instanz, die zurückgegeben werden könnte.
Das Delegationsmodell
Class Loader folgen dem Parent-First-Delegations-Modell. Wenn ein Loader aufgefordert wird, eine Klasse zu laden, versucht er nicht sofort, sie zu finden. Er fragt zuerst seinen Elternteil, der wiederum seinen Elternteil fragt, bis hinauf zum Bootstrap. Nur wenn kein Vorfahre die Klasse liefern kann, versucht der ursprüngliche Loader, sie selbst zu definieren.
// Conceptual shape of ClassLoader.loadClass:
protected Class<?> loadClass(String name, boolean resolve) {
Class<?> c = findLoadedClass(name); // already loaded? reuse it
if (c == null) {
try {
c = parent.loadClass(name); // delegate UP first
} catch (ClassNotFoundException e) {
c = findClass(name); // only now load it myself
}
}
return c;
}Diese Delegation stellt sicher, dass Kerntypen einmal geladen werden, vom höchsten Loader, der sie liefern kann. Deshalb können Sie java.lang.String nicht überschreiben, indem Sie Ihr eigenes String.class auf den Classpath legen — der Bootstrap-Loader beansprucht den Namen zuerst.
Laden, Verknüpfen, Initialisieren
Eine Klasse zum Leben zu erwecken geschieht in drei Phasen, und sie sind nicht dasselbe:
- Laden — den Bytecode lesen und das
Class-Objekt erstellen. - Verknüpfen — den Bytecode verifizieren, dass er wohlgeformt ist, statische Felder mit Standardwerten vorbereiten und symbolische Referenzen auflösen.
- Initialisieren — statische Initialisierer und statische Feldzuweisungen ausführen (die
<clinit>-Methode der Klasse).
Die entscheidende praktische Tatsache: Die Initialisierung ist lazy und geschieht genau einmal. Eine Klasse wird nur bei der ersten aktiven Verwendung initialisiert — beim ersten new, beim ersten statischen Methodenaufruf oder beim ersten Lesen eines nicht-konstanten statischen Feldes.
class Config {
static final Map<String, String> SETTINGS = load(); // runs once, on first touch
static Map<String, String> load() {
System.out.println("Config initialized");
return Map.of("env", "prod");
}
}
// "Config initialized" prints only when Config is first actively used.Benutzerdefinierte Class Loader
Sie können ClassLoader erweitern, um Klassen von überall zu laden — aus einer Datenbank, einem Netzwerk-Stream, generiertem Bytecode oder einem verschlüsselten JAR. Die zwei relevanten Methoden sind findClass (die Bytes finden und definieren) und defineClass (die rohen Bytes an die JVM übergeben, die eine Class zurückgibt).
class BytesLoader extends ClassLoader {
private final byte[] bytecode;
BytesLoader(byte[] bytecode) { this.bytecode = bytecode; }
@Override
protected Class<?> findClass(String name) {
return defineClass(name, bytecode, 0, bytecode.length);
}
}URLClassLoader ist die eingebaute Version dieser Idee — zeigen Sie auf JARs oder Verzeichnisse und er lädt Klassen bei Bedarf:
URL jar = Path.of("plugin.jar").toUri().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
Class<?> plugin = loader.loadClass("com.example.Plugin");
Object instance = plugin.getDeclaredConstructor().newInstance();
}Klassenidentität: Name plus Loader
Hier ist die Feinheit, die viele Menschen überrascht: Die Laufzeitidentität einer Klasse ist ihr vollqualifizierter Name und der Loader, der sie definiert hat. Laden Sie Bytes für Widget durch zwei verschiedene Loader, und Sie erhalten zwei unterschiedliche Class-Objekte — nicht gleich, nicht zuweisungskompatibel — obwohl beide aus identischem Bytecode stammen. Genau so isolieren Applikationsserver zwei eingesetzte Anwendungen, die beide eine Klasse namens com.acme.Util enthalten.
Ein ausgearbeitetes Beispiel: Loader, Delegation, Laziness und Identität
Dieses Programm benötigt keine externen Klassen — es verwendet die bereits in jeder JVM vorhandenen Loader. Es durchläuft die Loader-Kette, beweist, dass Kernklassen vom Bootstrap-Loader stammen, zeigt, wie Delegation dasselbe Class-Objekt zurückgibt, beobachtet, wie ein statischer Initialisierer lazy und nur einmal ausgeführt wird, und definiert dann denselben handgefertigten Bytecode durch zwei Loader, um die Name-plus-Loader-Identitätsregel zu beweisen.
Was aus dem Programmablauf zu entnehmen ist:
- Die ausgegebene Loader-Kette ist die lebendige Class-Loader-Hierarchie mit Ihrem Code am unteren Ende:
ClassLoadingDemowurde von einem Loader auf Anwendungsebene definiert, dessengetParent()der nächste Loader in der Kette ist. Jeder Loader kennt nur seinen Elternteil, und die Kette steigt immer in Richtung Bootstrap. String.class.getClassLoader()gibtnullaus, die JVM-Methode zu sagen: "geladen vom Bootstrap-Loader." Kern-JDK-Typen melden hier immernull; ein Objekt würde bedeuten, dass sie von einem niedrigeren Loader stammen, was nie der Fall ist.app.loadClass("java.lang.StringBuilder") == StringBuilder.classisttrue. Die Delegation hat die Anfrage an den Loader weitergeleitet, derStringBuilderbereits besitzt, sodass Sie das identischeClass-Objekt zurückbekommen, keine Kopie — Beweis, dass Delegation verhindert, dass Kerntypen zweimal geladen werden.Lazy <clinit> runningwird einmal ausgegeben, zwischen der Markierung--- referencing Lazy now ---und dem erstenLazy.VALUE = 42, und beim zweiten Lesen nie wieder. Die Initialisierung ist lazy (sie wartete bis zur ersten Verwendung) und idempotent (der statische Block wird genau einmal pro Loader ausgeführt).aundbheißen beideWidget, docha == bistfalseunda.isAssignableFrom(b)istfalse. Zwei Loader haben denselben Bytecode in zwei unterschiedliche Typen definiert — konkreter Beweis, dass die Laufzeitklassenidentität vollqualifizierter Name plus definierender Loader ist, der Mechanismus hinter der Classpath-Isolation in Applikationsservern.
Übungen
Verwandte Themen
Class Loading liegt an der Grenze zwischen der JVM und Ihrem Code und berührt daher mehrere benachbarte Themen:
- JVM-Architektur — wo das Class-Loader-Subsystem unter der Ausführungs-Engine und den Laufzeitdatenbereichen eingeordnet ist.
- Java Memory Model — wie geladene Klassen und ihre statischen Daten im Speicher leben.
- Garbage Collection — Class Loader (und ihre Klassen) können selbst entladen werden, wenn sie nicht mehr referenziert werden.
- Moduleinführung — auf dem Modulpfad wird das Laden durch die Modul-Lesbarkeit statt durch einen flachen Classpath gesteuert.
- Reflection-Einführung —
Class.forNameundloadClasssind die Einstiegspunkte, auf denen Reflection aufbaut.