Warum ClassLoader verstehen?
- Korrekte Funktionsweise von Plugins, Modulen, SPI/ServiceLoader, DI-Frameworks.
- Diagnose von ClassNotFoundException / NoClassDefFoundError / LinkageError.
- Isolation von Abhängigkeiten in Anwendungsservern, OSGi, Agentierung, Hot-Reload.
- Bewusste Arbeit mit classpath/module-path und
getResource*.
Hierarchie der Loader und Delegationsmodell
Hauptloader (JDK 9+)
- Bootstrap ClassLoader
— Native (in C++), lädt das JDK-Kern (
java.baseusw.). In Java dargestellt alsnull. - Platform ClassLoader (früher ExtensionClassLoader vor Java 8) — Lädt Plattformmodule des JDK (nicht Basis), Erweiterungen.
- Application (System) ClassLoader — Lädt Klassen der Anwendung aus dem classpath (oder module-path) — deine JARs/Klassen.
Überprüfen im Runtime:
ClassLoader app = ClassLoader.getSystemClassLoader();
ClassLoader platform = app.getParent();
ClassLoader bootstrap = platform.getParent(); // null → das ist Bootstrap
System.out.println(app); // Application
System.out.println(platform); // Platform
System.out.println(bootstrap); // nullDelegationsmodell (Parent Delegation)
Der Standardalgorithmus loadClass(name):
- Delegiere an den Parent (
parent.loadClass(name)). - Wenn der Parent den Klassennamen nicht findet — versuche, ihn selbst zu finden (
findClass(name)).
Dies verhindert, dass Basisklassen des Kerns überschrieben werden: deine Klassen “überschreiben” nicht java.lang.String.
Ladepfade: classpath vs module-path
- Classpath: bis Java 8 (und auch jetzt) — Liste von Verzeichnissen/JARs.
Option:
-cp/CLASSPATH. - Module-path (Java 9+): Modulsystem JPMS.
Option:
--module-path. Kontrolle über den Export von Paketen (exports) und das Öffnen für Reflexion (opens).
Wenn ein Klasse auf dem module-path sichtbar ist, aber das Paket nicht exportiert wurde, erhält man InaccessibleObjectException/IllegalAccess bei Reflexion oder ClassNotFoundException bei der Auflösung von Modulabhängigkeiten.
Lebenszyklus einer Klasse in der JVM
- Loading — Suche nach Bytecode und Erstellung von
Class<?>überdefineClass. - Linking = Verify (Bytecode-Verifizierung) + Prepare (Zuweisung/Initialisierung statischer Felder mit Nullen) + Resolve (Auflösung von Verweisen auf andere Typen bei der Referenzierung).
- Initialization — Ausführung von
<clinit>: statische Initialisierer/statische Felder mit Werten.
Die Initialisierung beginnt bei der ersten aktiven Verwendung: new, Aufruf eines static-Feldes/Methode usw.
Thread Context ClassLoader (TCCL)
TCCL (Thread.currentThread().getContextClassLoader()) — der “kontextuelle” Loader, den Frameworks verwenden, um Klassen/Ressourcen “von der Anwendung” und nicht “von oben” zu suchen.
Anwendung:
- ServiceLoader, XML-Parser, Logger, JDBC-Treiber, SPI.
- In Containern (z.B. Servlet-Container) stellt der Container den TCCL für den ClassLoader der Webanwendung bereit.
Beispiel:
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
URL res = tccl.getResource("config/app.yml");Ressourcen suchen: getResource vs getResourceAsStream
SomeClass.class.getResource("x.txt")— Pfad relativ zum PaketSomeClass.SomeClass.class.getResource("/x.txt")— Pfad relativ zum Wurzelverzeichnis des classpath.ClassLoader.getResource("x.txt")— immer relativ zum Wurzelverzeichnis.
Pattern:
try (var in = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream("config/app.yml")) {
// read thread
}Benutzerdefinierter ClassLoader: wann und wie
Wann er benötigt wird:
- Plug-in-System / „Mandanten“ mit unterschiedlichen Bibliotheksversionen.
- Laden von Klassen aus einer Datenbank, über das Netzwerk oder aus verschlüsselten JARs.
- Abhängigkeitsisolierung, Hot-Reload.
Minimales Beispiel (korrekte Delegation an den Eltern und benutzerdefinierte
findClass):
public class ByteArrayClassLoader extends ClassLoader {
private final Map<String, byte[]> defs;
public ByteArrayClassLoader(ClassLoader parent, Map<String, byte[]> defs) {
super(parent); // delegation to the parent
this.defs = defs;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = defs.get(name);
if (bytes == null) throw new ClassNotFoundException(name);
return defineClass(name, bytes, 0, bytes.length);
}
}
// Using:
Map<String, byte[]> code = Map.of("demo.Hello", compileBytes(...));
ClassLoader parent = Thread.currentThread().getContextClassLoader();
Class<?> helloCls = new ByteArrayClassLoader(parent, code).loadClass("demo.Hello");
Object hello = helloCls.getDeclaredConstructor().newInstance();Brich die „Top-down“-Delegation niemals ohne triftigen Grund — man landet sonst schnell in der „Classloader-Hölle“.
Häufige Fehler und wie man sie voneinander unterscheidet
| Symptom | Wann es auftritt | Wie man darüber nachdenken sollte |
|---|---|---|
| ClassNotFoundException | Klasse wird beim namenbasierten Laden nicht gefunden (Class.forName, loadClass) | Nicht im Classpath/Modulpfad, anderer Loader, falscher TCCL |
| NoClassDefFoundError | Die Klasse wurde zuvor gefunden, kann aber beim Zugriff nicht mehr initialisiert/gefunden werden | Initialisierung fehlgeschlagen (ExceptionInInitializerError) oder Klasse ist verschwunden/nicht sichtbar für diesen Loader |
LinkageError (NoSuchMethodError, IncompatibleClassChangeError, ClassFormatError) | Klassen-/JAR-Versionskonflikt, Signaturinkompatibilität | Zwei Versionen derselben Klasse in unterschiedlichen Loadern, „JAR-Hölle“ |
| ClassCastException “between identical names” | Dieselbe Klasse wird von verschiedenen ClassLoadern geladen | In der JVM besteht der Typ aus {className, classLoader}. Zwei identische Namen → unterschiedliche Typen |
| Diagnose: |
System.out.println(obj.getClass().getClassLoader());
// or
System.out.println(SomeClass.class.getClassLoader());Container und Isolierung (Tomcat/Jetty/Spring Boot)
- Jede Webanwendung hat ihren eigenen ClassLoader. Dadurch können verschiedene Bibliotheksversionen auf demselben Server koexistieren.
- Der Container setzt den TCCL auf den ClassLoader der Webanwendung, bevor ein Servlet oder Filter aufgerufen wird.
- Viele SPIs (JDBC-Treiber, JAXB, JAXP, Logger) laden Klassen über den TCCL. Wenn der TCCL nicht gesetzt ist, tritt eine
ClassNotFoundExceptionauf.
Fat JAR (Spring Boot):
- Enthält einen einzigen „Launcher“ mit eigenem JAR-Resolver → eine dedizierte ClassLoader-Hierarchie.
- Beim Debuggen von Klassen „aus einem JAR im JAR“ sollte man auf den
LaunchedURLClassLoaderachten.
Modulsystem (Java 9+) und reflektiver Zugriff
exports: macht ein Paket für Kompilierung/Verlinkung in anderen Modulen sichtbar.opens: gibt ein Paket für Reflexion frei (z. B. für Hibernate, Jackson).- JVM-Optionen zur Umgehung der Beschränkungen:
--add-opens module/package=targetModule(oderALL-UNNAMEDfür die nicht-modulare Welt). - Andernfalls treten
InaccessibleObjectExceptionoderIllegalAccessExceptionauf, wenn mansetAccessible(true)aufruft, selbst wenn sich die Klasse im Pfad befindet.
Service Provider Interface (SPI) und ServiceLoader
public interface PaymentGateway { void pay(); }
// META-INF/services/com.example.PaymentGateway
// contains a line with the FQN of the implementation: com.example.impl.StripeGateway
ServiceLoader<PaymentGateway> loader =
ServiceLoader.load(PaymentGateway.class, Thread.currentThread().getContextClassLoader());
for (PaymentGateway gw : loader) {
gw.pay();
}⚠️TCCL ist entscheidend – sonst kann der ServiceLoader keine Provider aus deinem ClassLoader „sehen“.
Beste Praktiken
- Beachte das Delegationsprinzip nach oben: überschreibe
findClass, nichtloadClass(außer du weißt genau, was du tust). - Protokolliere den ClassLoader bei Fehlern: wo die Klasse oder Ressource gesucht wurde.
- Verwende TCCL in Frameworks/SPIs:
Thread.currentThread().getContextClassLoader(). - Diagnostiziere Konflikte: prüfe, ob dieselbe Klasse von verschiedenen Loaders geladen wird.
- Ressourcenladen:
ClassLoader.getResource*– Pfad vom Wurzelverzeichnis;Class#getResource*– relativ zum Paket. - Module: plane
exports/opensoder JVM-Flags--add-opensfür Reflexion. - Vermeide mehrere Versionen derselben JAR im selben Pfad: „JAR-Hölle“ →
LinkageError. - Schließe
URLClassLoader(close()), wenn du temporäre Loader (Hot-Reload/Plugins) erstellst – sonst entstehen Speicherlecks. - In Spring Boot: beachte den benutzerdefinierten Loader des Launchers; für native Tests entpacke das „Fat JAR“ auf einen Standard-Classpath (
repackage=false-Profil). - Plugin-Isolierung: verwende einen separaten ClassLoader pro Plugin mit einem übergeordneten Loader für die API; reduziere Überschneidungen auf ein Minimum.
Mini-Beispiel: Diagnose „Die Klasse existiert, ist aber nicht sichtbar“
Symptom: NoClassDefFoundError: Lcom/x/Foo; beim Aufruf von Bibliothekscode.
Schritt für Schritt:
System.out.println(Foo.class.getClassLoader());
// Where lies?
System.out.println(Foo.class.getProtectionDomain().getCodeSource().getLocation());
// Who call?
for (var el : Thread.currentThread().getStackTrace())
System.out.println(el);
// check TCCL
System.out.println(Thread.currentThread().getContextClassLoader());
// check dublications
Enumeration<URL> urls =
Thread.currentThread().getContextClassLoader().getResources("com/x/Foo.class");
while (urls.hasMoreElements()) System.out.println(urls.nextElement());Wenn es mehrere Pfade gibt — hast du Duplikate. Wenn verschiedene Loader beteiligt sind — liegt ein Isolationskonflikt vor.