Kolumne: Java-Trickkiste

Aus der Java-Trickkiste: Class Loading

Arno Haase

Java lädt und initialisiert Klassen über einen ausgefeilten Mechanismus. Damit kommt man zwar im Programmieralltag nicht oft in Berührung, es hilft aber z. B. dabei, das Verhalten von Application Servern zu verstehen.

Expliziter Class Loader

Moderne IDEs und Build-Werkzeuge wie Maven machen es einfach, Bibliotheken in den Classpath aufzunehmen. Man kann deren Klassen dann einfach verwenden – das ist ja Sinn der Sache.

Man kann aber auch Klassen laden und verwenden, die nicht im Classpath liegen. Als Beispiel dient die Klasse NodeCachingLinkedList aus der commons-collections-Bibliothek von Apache, eine besondere Implementierung von java.util.List; ihre Besonderheiten sind für unser Beispiel egal. Die Bibliothek liegt zum Download bereit.

Um eine Klasse zu laden, brauchen wir einen ClassLoader (Listing 1) – der Code soll ja funktionieren, ohne dass commons-collections im Classpath liegt. Dazu erzeugen wir einen URLClassLoader, der im Konstruktor den Pfad zur JAR-Datei erhält.

Listing 1
final URLClassLoader cl = new URLClassLoader (new URL[] {new URL("file:///home/arno/tmp/commons-collections4-4.0.jar")});

final Class cls = cl.loadClass ("org.apache.commons.collections4.list.NodeCachingLinkedList");
final List coll = (List) cls.newInstance ();

coll.add ("a");
coll.add ("b");
for (String s: coll) {
  System.out.println (s);
}

System.out.println (coll.getClass ());
System.out.println (coll.getClass().getSuperclass ());
assert (coll instanceof java.util.List);

Zum Laden einer Klasse ruft der Code dann die Methode loadClass() des ClassLoaders auf und übergibt dabei den voll qualifizierten Namen der Klasse. Dieser Aufruf liefert eine Class zurück. NodeCachingLinkedList hat einen No-Args-Konstruktor, also können wir durch Aufruf von cls.newInstance() eine neue Instanz erzeugen.

Ab diesem Punkt ist die neu erzeugte Liste ein Objekt wie jedes andere – man kann Elemente hinzufügen oder löschen, und man kann über die Elemente iterieren. Auch Reflection funktioniert auf der dazugehörigen Klasse ohne Einschränkung. Und wenn man sich mit getClass().getSuperclass() die Basisklasse holt – AbstractLinkedList, ebenfalls aus commons-collections – dann sieht man, dass der ClassLoader diese Klasse automatisch mit geladen hat, ohne dass man sich darum kümmern musste.

Der Code in Listing 1 funktioniert übrigens auch, wenn man in der ersten Zeile den oben verlinkten Download-URL einträgt – dann lädt der URLClassLoader die Klassen direkt über das Netzwerk, ohne dass die JAR-Datei lokal im Dateisystem liegen müsste. Das ist natürlich erheblich langsamer, man kann Probleme mit Proxies haben, und die Anwendung lässt sich nur starten, wenn ein Third-Party-Server verfügbar ist. Aber es zeigt, dass nicht alle Klassen von einem lokalen Classpath kommen müssen.

Isolation

Wenn man auf demselben URLClassLoader mehrmals loadClass() für dieselbe Klasse aufruft, erhält man immer dasselbe Objekt zurück. Wenn man dieselbe Klasse aber über verschiedene ClassLoader lädt, dann sind das für die JVM komplett verschiedene Klassen (Listing 2).

Listing 2
final URLClassLoader loader1 = new URLClassLoader (new URL[] {new URL("file:///home/arno/tmp/commons-collections4-4.0.jar")});
final URLClassLoader loader2 = new URLClassLoader (new URL[] {new URL("file:///home/arno/tmp/commons-collections4-4.0.jar")});

final Class cls = loader1.loadClass ("org.apache.commons.collections4.list.NodeCachingLinkedList");
final Class cls1 = loader1.loadClass ("org.apache.commons.collections4.list.NodeCachingLinkedList");
final Class cls2 = loader2.loadClass ("org.apache.commons.collections4.list.NodeCachingLinkedList");

assert (cls == cls1);
assert (cls != cls2);

final List l = (List) cls.newInstance ();
assert (cls.isInstance (l));
assert (!cls2.isInstance (l));

Der zweite ClassLoader erzeugt ein komplett separates Class-Objekt, das alle statischen Variablen ein zweites Mal enthält und ggf. initialisiert. Und wenn man auf Basis des ersten ClassLoaders eine Instanz erzeugt, ist die keine Instanz der Klasse, die der zweite ClassLoader geladen hat.

Diese vollständige Isolation zwischen ClassLoadern machen sich Application Server zunutze, um mehrere Webanwendungen im selben Container zu deployen, ohne dass die sich gegenseitig sehen oder in die Quere kommen könnten.

Jede Anwendung (inklusive ihrer Bibliotheken) wird mit einem eigenen ClassLoader geladen, und selbst wenn mehrere Anwendungen dieselben Klassen verwenden, existieren sie friedlich nebeneinander. Das gilt sogar dann, wenn sie statische Variablen verwenden oder verschiedene Versionen derselben Bibliothek benutzen.

Reflection

Wenn man einen String mit einem Klassennamen hat und die dazugehörige Klasse laden will, braucht man einen ClassLoader. Und wenn man in einer Anwendung mit mehreren ClassLoadern den falschen verwendet, führt das zur Laufzeit zu Fehlern – z. B. weil er die Klasse nicht findet.

Ein naiver Ansatz besteht darin, mit getClass().getClassLoader() den ClassLoader der aktuellen Klasse zu verwenden. Die Methode Class.forName(…) tut dasselbe, wenn auch mit weniger syntaktischem Overhead. Beides funktioniert aber in nicht trivialen Anwendungen nicht zuverlässig.

Das ist besonders bei Frameworks wie Hibernate oder Spring der Fall, bei denen ein Framework per Reflection auf Anwendungsklassen zugreift. Wenn das Framework als Teil des Application Servers installiert ist, „sieht“ der ClassLoader des Frameworks die Anwendungsklassen nicht.

Deshalb gibt es den so genannten Context Class Loader: Wenn man Thread.currentThread().getContextClassLoader() aufruft, erhält man den ClassLoader, der zum Laden von Anwendungsklassen geeignet ist. Wenn man nicht sehr sicher ist, dass man weiß, was man tut, sollte man zum Laden von Klassen immer diesen ClassLoader verwenden.

Dieser Context Class Loader ist unter der Oberfläche einfach eine Variable je Thread, die man mit setContextClassLoader(…) auch setzen könnte. Das übernehmen aber Anwendungsframeworks und Application Server, und man kann und sollte davon ausgehen, dass die Variable sinnvoll gefüllt ist.

Per Default ist sie mit dem AppClassLoader initialisiert. Man kann sie also auch in kleinen, einfachen Anwendungen ohne Frameworks guten Gewissens verwenden.

Aufräumarbeiten

Jeder ClassLoader hält intern eine Liste aller von ihm geladenen Klassen und schützt diese dadurch vor der Garbage Collection. Erst wenn der ClassLoader selbst freigegeben wird, können auch seine Klassen abgeräumt werden. Das ist wichtig, weil dadurch die Inhalte von statischen Variablen erhalten bleiben, auch wenn es vorübergehend keine Instanzen einer Klasse gibt.

Umgekehrt hat jede Klasse eine Referenz auf „ihren“ ClassLoader. Der kann also nicht von der GC abgeräumt werden, solange es auch nur eine einzige Referenz „von außen“ auf eine einzige Instanz einer seiner Klassen gibt. Das kann zum Beispiel eine nicht aufgeräumte ThreadLocal-Variable sein oder ein Hintergrundthread.

Solche Referenzen, die eine GC eines ClassLoaders verhindern, verhindern das saubere Undeployment von Anwendungen in Application Servern und machen sich dort als Memory Leak bemerkbar.

Hierarchie

ClassLoader bilden eine Hierarchie: Jeder ClassLoader hat einen (optionalen) Parent ClassLoader, und wenn er versucht, eine Klasse zu laden, fragt er diesen typischerweise zuerst, ob der die Klasse kennt. Dadurch wird z. B. die Java-Standardbibliothek immer vom selben ClassLoader geladen, und java.lang.String ist immer und überall dieselbe Klasse.

Wie die ClassLoader-Hierarchie einer neu gestarteten Anwendung aussieht, kann jede JVM selbst frei entscheiden. Listing 3 gibt diese Hierarchie aus.

Listing 3
ClassLoader loader = getClass().getClassLoader();
while (loader != null) {
  System.out.println (loader.getClass().getName());
  loader = loader.getParent();
}
System.out.println (String.class.getClassLoader());

Der Code beginnt mit dem ClassLoader, der die Anwendungsklasse geladen hat. Den gibt er aus, holt sich den Parent ClassLoader und wiederholt das Ganze, solange dieser nicht null ist. Schließlich gibt der Code noch den ClassLoader aus, mit dem Klassen aus der Standardbibliothek geladen werden.

Auf der Oracle-JVM ist das eine dreistufige Hierarchie. Anwendungsklassen werden von einer Instanz von AppClassLoader geladen, die den Classpath der Anwendung kennt und bedient. Darunter liegt eine Instanz von ExtClassLoader, der für Bibliotheken in den Extension Directories der JVM zuständig ist: Bibliotheken, die z. B. im Verzeichnis lib/ext der JRE liegen, stehen auf diesem Weg allen Java-Anwendungen zur Verfügung.

Schließlich gibt es noch den Bootstrap ClassLoader, der für die Standardbibliotheken zuständig ist. Der ist aber selbst kein Java-Objekt, sonst müsste er sich ja selbst laden. Deshalb liefert der Aufruf von getClassLoader() für eine Klasse aus der Standardbibliothek null zurück. Dieser Bootstrap ClassLoader ist übrigens der einzige, der Klassen aus dem Wurzelpackage java liefern darf. Die Klasse ClassLoader überprüft das hart in der Methode preDefineClass().

Es ist aufschlussreich, den Code aus Listing 3 einmal im Application Server auszuführen. Dann sieht man, was für eine Hierarchie von ClassLoadern dieser benutzt, um Webanwendungen voneinander zu isolieren.

Fazit

Java lädt alle Klassen durch ClassLoader, die hierarchisch angeordnet sind. Code aus verschiedenen ClassLoadern ist vollständig voneinander isoliert, und besonders Application Server nutzen das aus, um mehrere Anwendungen in derselben JVM deployen zu können.

Auch wenn man mit diesen Mechanismen beim alltäglichen Programmieren selten direkt in Berührung kommt, ist es hilfreich, sie zu kennen – spätestens wenn der Application Server sich überraschend verhält und man sein Verhalten verstehen will. Oder einfach, weil man neugierig ist und gerne versteht, wie Dinge funktionieren.

Geschrieben von
Arno Haase
Arno Haase
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationaler Datenbank und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: