RU | EN | DE

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+)

  1. Bootstrap ClassLoader — Native (in C++), lädt das JDK-Kern (java.base usw.). In Java dargestellt als null.
  2. Platform ClassLoader (früher ExtensionClassLoader vor Java 8) — Lädt Plattformmodule des JDK (nicht Basis), Erweiterungen.
  3. 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); // null

Delegationsmodell (Parent Delegation)

Der Standardalgorithmus loadClass(name):

  1. Delegiere an den Parent (parent.loadClass(name)).
  2. 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

  1. Loading — Suche nach Bytecode und Erstellung von Class<?> über defineClass.
  2. Linking = Verify (Bytecode-Verifizierung) + Prepare (Zuweisung/Initialisierung statischer Felder mit Nullen) + Resolve (Auflösung von Verweisen auf andere Typen bei der Referenzierung).
  3. 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 Paket SomeClass.
  • 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

SymptomWann es auftrittWie man darüber nachdenken sollte
ClassNotFoundExceptionKlasse wird beim namenbasierten Laden nicht gefunden (Class.forName, loadClass)Nicht im Classpath/Modulpfad, anderer Loader, falscher TCCL
NoClassDefFoundErrorDie Klasse wurde zuvor gefunden, kann aber beim Zugriff nicht mehr initialisiert/gefunden werdenInitialisierung fehlgeschlagen (ExceptionInInitializerError) oder Klasse ist verschwunden/nicht sichtbar für diesen Loader
LinkageError (NoSuchMethodError, IncompatibleClassChangeError, ClassFormatError)Klassen-/JAR-Versionskonflikt, SignaturinkompatibilitätZwei Versionen derselben Klasse in unterschiedlichen Loadern, „JAR-Hölle“
ClassCastException “between identical names”Dieselbe Klasse wird von verschiedenen ClassLoadern geladenIn 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 ClassNotFoundException auf.

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 LaunchedURLClassLoader achten.

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 (oder ALL-UNNAMED für die nicht-modulare Welt).
  • Andernfalls treten InaccessibleObjectException oder IllegalAccessException auf, wenn man setAccessible(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, nicht loadClass (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/opens oder JVM-Flags --add-opens fü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.