Kolumne

Aus der Java-Trickkiste: Class Loading Reloaded

Arno Haase
java-trickkiste

In der letzten Folge der Java-Trickkiste habe ich über Class Loading geschrieben. Darauf ist von mehreren Seiten die Bitte gekommen, das Thema zu vertiefen. Das mache ich gern und blicke dabei hinter die Kulissen einiger Frameworks.

Standardablauf

Der Ablauf beim Laden von Klassen ist in der abstrakten Klasse java.lang.ClassLoader festgelegt. Alle konkreten ClassLoader-Implementierungen müssen von ihr erben. Sie implementiert das Standardverhalten, das eine ganze Reihe von Möglichkeiten zum Customizing vorsieht.

Zunächst ruftdie Methode loadClass() die JVM auf. Das geschieht immer dann, wenn sie eine Klasse benötigt. Listing 1 zeigt ihren vereinfachten Quelltext aus den JDK-Sourcen.

private final ClassLoader parent;
...
Class<?> loadClass (String name, boolean resolve) 
        throws ClassNotFoundException {
  synchronized (...) {
    Class<?> c = findLoadedClass (name);
    if (c == null) {
      try {
        if (parent != null) {
          c = parent.loadClass(name, false);
        } else {
          c = findBootstrapClass(name);
        }
      }
      catch (ClassNotFoundException e) { }
      if (c == null) {
        c = findClass (name);
      }
    }
    if (resolve) {
      resolveClass (c);
    }
    return c;
  }
}

protected native final Class<?> findLoadedClass(String name);
private native Class<?> findBootstrapClass(String name);
protected native final void resolveClass(Class<?> c);
  
protected Class<?> findClass(String name) throws ClassNotFoundException {
  throw new ClassNotFoundException(name);
}
...

Ganz außen in der Methode steht ein synchronized-Block. Er schützt den ClassLoader vor nebenläufigen Zugriffen. Die Details sehen wir uns weiter unten an.

Zum Laden der Klasse sucht der Code der Reihe nach an drei Stellen. Als Erstes fragt er mit einem Aufruf der native-Methode findLoadedClass() nach, ob dieser ClassLoader die Klasse schon einmal geladen hat. Dieses Caching beschleunigt Programme extrem und stellt sicher, dass es jede Klasse nur einmal je ClassLoader gibt. Wenn die Klasse noch nicht im Cache liegt, fragt er bei seinem Parent-ClassLoader oder dem System-ClassLoader nach. Führt auch das nicht zum Ziel, ruft er schließlich findClass() auf. So versucht der ClassLoader, die angefragte Klasse bereitzustellen. Diese Methode wirft per Default immer eine ClassNotFoundException, und ein konkreter ClassLoader muss diese implementieren – z. B. so, dass sie Code aus einer JAR-Datei lädt.

Der letzte Schritt beim Laden einer Klasse ist der optionale Aufruf von resolveClass(). Diese Methode lädt alle referenzierten Klassen nach, beispielsweise die Basisklasse, Rückgabetypen von Methoden und die Typen von lokalen Variablen. Wenn dabei Typen fehlen oder nicht passen, kann es zu Fehlern wie NoSuchMethodError oder NoClassDefFoundError kommen. Es ist übrigens ausdrücklich erlaubt, resolve() mehrmals für dieselbe Klasse aufzurufen. Es ist daher kein Problem, wenn loadClass() sie am Ende einfach aufruft.

HotSpot führt diese Auflösung von Klassen per Default nicht automatisch beim Laden aus, sondern erst beim ersten Zugriff auf eine Klasse. Das hat den Vorteil, dass Anwendungen schneller starten und weniger Speicher belegen.

Von Bytes zu Klassen

Die Klasse ClassLoader selbst kümmert sich um das Delegieren, Caching und Linking von Klassen. Sie kann beispielsweise nicht Klassen aus Dateien laden; dazu müssen Subklassen die Methode findClass() überschreiben.

So eine Subklasse ist URLClassLoader. Sie liest den Inhalt von .class-Dateien , die direkt im Dateisystem oder in JAR-Dateien liegen oder anders per URL erreichbar sind. Ihre tatsächliche Implementierung ist kompliziert, weil sie viel Code für Security, Fehlerbehandlung sowie einige Performanceoptimierungen enthält. Listing 2 zeigt die Essenz.

private final URLClassPath ucp;
...

protected Class<?> findClass(String name) throws ClassNotFoundException {
  String path = name.replace ('.', '/').concat (".class");
  Resource res = ucp.getResource (path, false);
  if (res != null) {
    try {
      final ByteBuffer bb = res.getByteBuffer ();
      int len = bb.remaining();
      byte[] bytes = new byte[len];
      bb.get (bytes);  
      return super.defineClass (name, bytes, 0, len);
    }
    catch (IOException e) {
      throw new ClassNotFoundException (name, e);
    }
  } else {
    throw new ClassNotFoundException (name);
  }
}

Wenn die Methode findClass() mit dem Namen einer Klasse aufgerufen wird, dann übersetzt sie den punktseparierten Klassennamen in den dazugehörigen Dateinamen samt Pfad mit der Endung „.class“.

findClass() holt sich dann auf dem Umweg über die Klassen URLClassPath und Resource, die sich um I/O kümmern und mit dem Laden der Klasse nichts direkt zu tun haben, einen ByteBuffer mit dem Inhalt der Class-Datei. Den Inhalt der Datei, ein Bytearray, holt sie dann durch Aufruf von ByteBuffer.get().

findClass () erzeugt aus dem Bytearray dann durch Aufruf von super.defineClass(…) ein Class-Objekt. Die Klasse ClassLoader, von der URLClassLoader erbt, hat eine ganze Reihe von defineClass()-Methoden mit verschiedenen Parametern. Diese Methoden sind der einzige Weg, neue Class-Objekte zu erzeugen. Sie überprüfen, dass die übergebenen Bytes eine gültige Klassendefinition enthalten. Dabei stellen sie sicher, dass jeder Sprung-Bytecode eine gültige Zieladresse hat und dass das Typsystem eingehalten wird („Verify“). Anschließend belegen die defineClass()-Methoden den Speicher für das Class-Objekt („Prepare“).

Paralleles Class Loading

Die Default-Implementierung von ClassLoader.loadClass() schützt sich mit einem synchronized-Block vor parallelen Zugriffen. Vor Java 7 hatte jeder ClassLoader nur ein einziges Lock. Alle Klassen wurden nacheinander geladen. Wenn mehrere Threads das parallel versuchten, mussten sie eben aufeinander warten. Das war zwar robust, konnte aber das Starten von großen Anwendungen auf Computern mit vielen CPUs spürbar verzögern.

Deshalb registrieren ClassLoader-Implementierungenmit Java 7, dass sie mehrere Klassen parallel laden können. Dazu müssen sie aus einem static-Block heraus die Methode ClassLoader.registerAsParallelCapable() aufrufen, bevor die erste Instanz des ClassLoaders erzeugt wurde. Dann erfolgt die Synchronisation jeder geladenen Klasse. Auch wenn mehrere Threads gleichzeitig versuchen, eine Klasse zu laden, wird sie nur einmal erzeugt. Das kann für beliebig viele Klassen parallel erfolgen.

Dieser Mechanismus ist auf maximale Stabilität ausgerichtet: Die JVM nimmt per Default an, dass ein ClassLoader seriell arbeitet. Auch wenn ein ClassLoader parallel laden kann, wie z. B. der URLClassLoader, gilt das nicht automatisch für seine Subklassen.

Als zusätzliches Sicherheitsnetz gibt es den HotSpot-Schalter -XX:+AlwaysLockClassLoader, mit dem man paralleles Laden von Klassen komplett abschalten kann.

Parent Last

Wie wir oben gesehen haben, fragt ein ClassLoader im Normalfall zuerst seinen ParentClassLoader nach einer Klasse und wird selbst nur aktiv, wenn dieser die Klasse nicht liefern kann („Parent First“).

Dieses Default-Verhalten kann lästig sein, z. B. wenn ein Application Server eine alte Version einer Bibliothek mitbringt, während die Anwendung eine neuere Version verwenden will. „Parent First“ bedeutet in dieser Situation, dass der ClassLoader der deployten Anwendung immer die alte Version der Bibliothek liefert.

Man kann einen ClassLoader recht einfach implementieren, der zuerst versucht, eine Klasse selbst zu laden; schlägt dies fehl, fragt er oben in der Hierarchie nach. Application Server bieten typischerweise diese Möglichkeit.

class ChildFirstURLClassLoader extends URLClassLoader {
  public ChildFirstClassLoader(URL[] classpath, ClassLoader parent) {
    super(classpath, parent);
  }

  protected synchronized Class<?> loadClass(String name, boolean resolve) 
                             throws ClassNotFoundException {
    Class<?> c = findLoadedClass(name);
    if (c == null) {
      try {
        c = findClass(name);
      } catch (ClassNotFoundException e) {
        c = getParent().loadClass(name);
      }
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }
}

Der ChildFirstClassLoader in Listing 3 erweitert URLClassLoader und erbt damit die Funktionalität, Klassen über eine Liste von URLs aufzulösen. Um die Reihenfolge zu verändern, überschreibt er nur die Methode loadClass(). Dazu sieht der Code mit einem Aufruf von findLoadedClass() nach, ob er die Klasse schon einmal geladen hat. In diesem Fall gibt er den Wert aus seinem Cache zurück. Andernfalls ruft er die findClass()-Implementierung von URLClassLoader auf. Das ist sein Versuch, die Klasse selbst zu laden: Der Aufruf durchsucht die Liste der registrierten URLs nach der Klasse. Nur wenn das nicht funktioniert hat, ruft er loadClass() auf seinem Parent-ClassLoader auf.

Auf diese Weise haben wir einen ClassLoader, der eine „Parent Last“-Strategie beim Laden von Klassen anwendet. Bei verschiedenen Versionen einer Bibliothek in einem Application Server würde dieser ClassLoader der Anwendung „ihre“ Version der Bibliothek liefern.

Neben Klassen können ClassLoader auch „Ressourcen“ laden. Um dort zuerst lokal und dann beim Parent zu suchen, müssten wir zusätzlich die Methoden getResource() und getResources() überschreiben. Das funktioniert völlig analog zu loadClass().

Synthetische Klassen

Die meisten Klassen liegen irgendwo als fertige .class-Dateien. Die Aufgabe des ClassLoaders besteht darin, sie von dort einzulesen und dann bereitzustellen. Das muss aber nicht so sein – es ist jedem ClassLoader überlassen, auf welchem Weg er die Bytesequenz ermittelt, die er an defineClass() übergibt.

So gibt es z. B. kommerzielle Kopierschutzlösungen, die den Programmcode verschlüsselt auf der Festplatte ablegen. Sie verwenden intern ClassLoader, die einen Schlüssel aus einem Dongle auslesen und damit beim Laden die Klassen entschlüsseln.

Bibliotheken für Byte Code Enhancement wie cglib, Javassist oder ASM können bestehende Klassen verändern oder neue, komplett synthetische Klassen erzeugen. Sie enthalten eigene ClassLoader-Implementierungen, die diese Modifikationen zur Laufzeit anwenden und das Ergebnis bereitstellen. Diese ClassLoader verwenden natürlich eine „Parent Last“-Strategie – schließlich wollen sie ausdrücklich bestehende Klassen durch modifizierte Versionen ersetzen.

Hibernate verwendet solches Byte Code Enhancement z. B., um zur Laufzeit Proxy-Objekte für Lazy Loading zu erzeugen. Und Testbibliotheken wie PowerMock ermöglichen damit, private oder sogar statische Methoden von Objekten gezielt für einzelne Tests zu mocken.

Fazit

Angepasste ClassLoader sind eine Basistechnologie im Java-Universum. Sie wird von vielen Bibliotheken unter der Oberfläche eingesetzt, ohne dass man beim Programmieren damit direkt in Berührung kommt. Sie erlaubt es, Java um Features zu erweitern, die man auf anderen Wegen in dieser Einfachheit nicht hätte bauen können.

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
4000
  Subscribe  
Benachrichtige mich zu: