Java ClassLoader - Wesen, Funktionsweise und Anwendung

Philipp Hauer vom 21. Januar 2011. ©

Inhaltsangabe

Wesen

Der ClassLoader konstruiert Class-Objekte (java.lang.Class) aus Bytecode (byte-Array).

Eine Klasse in Java wird durch seinen voll qualifizierten Namen UND dem ClassLoader (der ihn geladen hat) identifiziert. Nicht allein durch seinen voll qualifizierten Namen.

Implikationen:

  1. Daher ist es möglich, dass mehrere Klassen mit dem selben Namen in einer VM gleichzeitig existieren, solange sie von verschiedenen ClassLoadern geladen wurden.
  2. Zwei Klassen, die mit verschiedenen ClassLoadern geladen wurden, sind von verschiedenen Typen (inkompatibel!), selbst wenn sie von ein und dem selben *.class-file geladen wurden.

Beweis:


public class KatzenTest {
    public static void main(String[] args) throws Exception{
        //bin/pkg/Katze.class ist bereits zum Compilezeitpunkt bekannt, d. h. im Classpath  
        //und wird damit vom System-Classloader geladen
        Katze katze = new Katze();
        
        //Bytecode der Katze holen
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("bin/pkg/Katze.class"));
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int zahl;
        while ((zahl = bufferedInputStream.read()) != -1){
            byteArrayOutputStream.write(zahl);
        }
        byte[] bytecode = byteArrayOutputStream.toByteArray();
        
        //ClassLoader, der keine Delegation unterstützt.
        MyClassLoader myClassLoader = new MyClassLoader();
        //Classobjekt laden.
        Class<?> katzeClass = myClassLoader.findClass("pkg.Katze", bytecode);
        
        //myClassLoader hat Katze geladen, daher ist er auch der ClassLoader von katzeClass
        ClassLoader classLoader = katzeClass.getClassLoader();
        System.out.println(classLoader);//"pkg.MyClassLoader@d0a5d9"!         
        //zum Vergleich, die compiletime Katze, der der System-ClassLoader geladen hat.
        System.out.println(katze.getClass().getClassLoader());//"sun.misc.Launcher$AppClassLoader@1a7bf11"         
        //--> Die Katzenklassen sind inkompatibel zu einander. Cast schlägt fehl!
        katze = (Katze) katzeClass.newInstance();//ClassCastException!
        //Exception in thread "main" java.lang.ClassCastException: pkg.Katze cannot be cast to pkg.Katze     }
    
    public static class MyClassLoader extends ClassLoader {
        public Class<?> findClass(String pQualifiedClassName, byte[] pBytecode) {
            return defineClass(pQualifiedClassName, pBytecode, 0, pBytecode.length);
        }
    }
}		

Dadurch können mehrere Klassen mit den gleichen Namen gleichzeitig existieren. Sie müssen lediglich durch andere ClassLoader geladen werden. In der Praxis instanziiert man einfach immer einen neuen ClassLoader. Somit können Namen "wiederverwendet" werden:


Class<?> autoClass = new MyClassLoader().loadClass("pkg.Auto");

Allgemein

  • java.lang.ClassLoader ist eine abstrakte Klasse. Um selbst Klassen zur Laufzeit zu laden, muss ClassLoader erweitert werden.
  • Wenn die JVM hochfährt, lädt der SystemClassLoader alle Klassen im ClassPath.
    • Memo: Schreibt man "new Auto()" wird der SystemClassLoader diese Klasse laden.

Mit der statischen Methode ClassLoader.getSystemClassLoader() kann eine Referenz auf den SystemClassLoader erhalten werden:


ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //"sun.misc.Launcher$AppClassLoader@11b86e7"

Liefert irgendeinObjekt.getClass().getClassLoader() null zurück, so wurde diese Klasse vom SystemClassLoader geladen.

Delegation

  • ClassLoader verfolgen das Konzept der Delegation.
  • Ein ClassLoader hat einen Parent-ClassLoader
  • Soll der ClassLoader eine Klasse laden, so delegiert er zunächst die Aufgabe an seinen Parent
  • Erst wenn der Parent die Klasse nicht finden kann, versucht er selbst die Klasse zu laden.
  • Der oberste Parent in der Delegationshierarchie ist der SystemClassLoader.

ClassLoader Hierarchie Vererbungshierarchie von ClasLoader und die Parentassoziation. (Frei nach [1], Seite 124)

API

Konstruktoren

java.lang.ClassLoader hat 2 Konstruktoren:


//parent ist der System-ClassLoader 
protected ClassLoader() 
//parent explizit setzen 
protected ClassLoader(ClassLoader parent) 

Beweis:


public class MyClassLoader extends ClassLoader { 
    public static void main(String[] args) { 
      System.out.println(new MyClassLoader().getParent()); //sun.misc.Launcher$AppClassLoader@11b86e7
  } 
} 		

Methoden-Übersicht

Methode Beschreibung
ClassLoader getParent() Finale Methode, die den Parent-ClassLoader zurückgibt.
ClassLoader getSystemClassLoader() Statische Methode, um den System-ClassLoader zu erhalten.
Class findClass(String name) Protected Methode, um ein Class-Objekt für eine bestimmte Klasse (voll qualifizierter Name) zu finden.
Class loadClass(String name) Public Methode. Startpunkt für das ClassLoading! Lädt Klasse mit einen bestimmten Namen.
Class defineClass(String name, byte[] b, int off, int len) Proteced final Methode. Konvertiert ein Bytearray (Bytecode!) in eine Instanz von Class. Mit dieser Methode kann aus Bytecode eine Klasse in die JVM geladen werden.
Class findLoadedClass(String name) Proteced final Methode. Findet Klasse mit dem gegebenen Namen, wenn diese Klasse bereits durch den ClassLoader selbst geladen wurde.

loadClass()

  • Mit der Methode loadClass() beginnt der Prozess des Ladens.
  • Die JVM lädt alle Klassen, in dem sie loadClass() aufruft.
  • Die Defaultimplementierung von loadClass() umfasst folgende Schritte:

loadClass()-Funktionweise als UML-Sequenzdiagramm Funktionsweise von loadClass() als UML-Sequenzdiagramm (frei nach [1], Seite 126)

  1. Aufruf von findLoadedClass(), um zu checken, ob die Klasse nicht bereits vom ClassLoader selbst geladen wurde. ClassLoader merken sich die Klassen, die sie selbst geladen haben. Subklassen erben dieses Verhalten.
  2. Wenn keine Klasse gefunden wurde, wird loadClass() auf dem Parent-ClassLoader aufgerufen. Die Defaultimplementierung von loadClass() unterstützt somit das Delegationsmodel.
  3. Wenn wieder keine Klasse gefunden wurde, wird findClass() aufgerufen. An dieser Stelle sollte man sich in seiner ClassLoader-Subklasse in den Loadingprozess einharken, wenn man das Delegationsmodell erhalten möchte. Mit der defineClass()-Methode kann hier eine Class aus Bytecode erzeugt werden.
  4. Wenn ein Schritt keine Class gefunden hat, wird eine ClassNotFoundException geworfen.

Begrifflichkeiten

  • Defining loader: ClassLoader, der die Class mit defineClass() produziert hat. Eine Referenz auf den defining loader kann mit folgender Methode erhalten werden:
    
    Class.getClassLoader() 
    
  • Initiating loader: Jeder ClassLoader, der in dem Prozess des Klassenladens involviert war. Durch das Delegationsmodel können dies mehrere sein.

Beispiel für einen eigenen ClassLoader

Mit Delegation

Dazu überschreiben wir findClass(String name). Das Problem dabei ist, dass wir in der Methode lediglich den Namen der gewünschten Klasse als Parameter erhalten und nicht etwas den Bytecode, den wir für defineClass() benötigen. Diese Informationen müssen wir uns dann ggf. via Konstruktor beschaffen.

Ohne Delegation

Hierbei definieren wir einen neue Methode findClass(String pQualifiedClassName, byte[] pBytecode). In dieser rufen wir die sonst von außen nicht zugängliche protected defineClass()-Methode auf. Achtung damit haben wir findClass() überladen und nicht überschrieben. Das "normale" findClass(String name) bleibt davon unangetastet.

Wir umgehen Delegation, in dem wir nicht loadClass(), sondern direkt unsere definierte Methode findClass() aufrufen.


public class MyClassLoader extends ClassLoader {
    
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        byte[] bytecode = new byte[]{0};
        //bytecode = ... Mit sinnvollen Bytecode füllen. Z.B.: *.class-File einlesen.         
        //Classobjekt laden.
        Class<?> className = new MyClassLoader().findClass("pkg.Klassenname", bytecode);
        //Instanziierung mit Reflection
        Object object = className.newInstance();
        //verwenden (z.B.: Reflektiver Methodenaufruf oder Cast auf Schnittstelle...)
        //...
    }
    
    /**
     * Lädt den Bytecode und lieft ein Classobjekt zurück.
     * Delegationsmodel wird nicht unterstützt.
     * @param pQualifiedClassName Beispiel: gui.components.MyButton
     * @param pBytecode
     * @return Classobjekt
     */
    public Class<?> findClass(String pQualifiedClassName, byte[] pBytecode) {
        return defineClass(pQualifiedClassName, pBytecode, 0, pBytecode.length);
    }
}		

Memos, Tipps und Best Practise

findClass()

Wenn Delegation gewünscht, überschreibe nur findClass()! loadClass() realisiert in der Defaultimplementierung das Delegationmodel und ruft findClass() selbst auf.

Allerdings wird dies schwierig, wenn man dem ClassLoader direkt Bytecode übergeben möchte, den er laden soll, da findClass() nur einen String entgegen nimmt. Der URLClassLoader löst dieses Problem, in dem die benötigten Informationen im Konstruktor entgegen genommen werden. Aber oft möchte man bewusst das Delegationmodel umgehen. Häufig will man nunmal den Bytecode geladen haben und nicht, dass ein Parent-ClassLoader eine Klasse mit "zufällig" dem selben Namen zurückgibt! Daher ist es üblich, einfach eine neue Public Methode zu definieren, die dem Vorhaben dienlich ist.


//diese Methode hat nichts mit findClass(String) von java.lang.ClassLoader zu tun!
public Class<?> findClass(String pQualifiedClassName, byte[] pBytecode) {
   return defineClass(pQualifiedClassName, pBytecode, 0, pBytecode.length);
}

BytecodeVarifier

Dass Java eine auf Sicherheit getrimmte Sprache ist, zeigt sich daran, dass selbst der Bytecode beim Laden mit defineClass() noch einmal gecheckt wird. Dies erledigt der BytecodeVarifier. Man braucht sich also keine Sorgen um fehlerhaften Bytecode machen, da dies vom ClassLoader mit einer VerifyException abgeschmettert wird.

Erfinde das Rad nicht neu!

Es gibt einige bereits implementierte ClassLoader-Subklassen, die genutzt werden können.

ClassLoader-Implementierungen in der Java-Bibliothek ClassLoader-Implementierungen in der Java-Bibliothek

URLClassLoader

Der URLClassLoader ermöglicht das Laden einer Klasse von einer bestimmten URL (dies schließt lokale Pfade selbstverständlich ein).


String url1 = "file://C:/path/pkg/Auto.class"; 
String url2 = "file://C:/path/pkg/LKW.class"; 
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { new URL(url1), new URL(url2) }); 
Class<?> newClass = urlClassLoader.loadClass("pkg.Auto"); 

Der URLClassLoader bewahrt das Delegationsprinzip. Den Bytecode der *.class-Datei erhält er in Form eines Pfades im Konstruktor. Demnach wird die *.class-Datei nur geladen, wenn die Klasse Auto nicht bereits von einem Parent-Classloader geladen wurde.

Beweis:


public class Auto { 
    public static void main(String[] args) throws Exception { 
        //bin/pkg/Auto.class ist bereits zum Compilezeitpunkt bekannt, d. h. im Classpath
        //und damit vom System-Classloader geladen 
        Auto auto = new Auto(); 
         
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://bin/pkg/Auto.class")}); 
        //Classobjekt laden. 
        Class<?> autoClass = urlClassLoader.loadClass("pkg.Auto"); 
         
        //wenn korrekt delegiert wurde, dann müsste der System-Classloader
        //die Klasse geladen haben und nicht der URLClassLoader 
        ClassLoader classLoader = autoClass.getClassLoader(); 
        System.out.println(classLoader);//"sun.misc.Launcher$AppClassLoader@11b86e7"!          
        //zum Vergleich 
        System.out.println(urlClassLoader);//"java.net.URLClassLoader@3e25a5"          
        //demnach gelingt ein Cast 
        auto = (Auto) autoClass.newInstance();//Keine Exception! 
    } 
} 		

Class.forName(String)

  • Merkhilfe: "get class for name"
  • Class.forName() ist eine Convenienceklasse, mit der Klassen anhand des Klassennamens geladen werden können. Dabei wird die Verwendung eines ClassLoaders gekapselt.
  • Vorteil: Name der Klasse muss nicht zur Compiletime bekannt sein.
  • Defaultmäßig sucht der System-ClassLoader im Classpath nach dem entsprechenden *.class-File

Class<?> newClass = Class.forName("pkg.Klassenname"); 

Literatur und Links

[1] Ira R. Forman, Nate Forman: Java Reflection in Action. Manning Publications Co. 2005.

[2] Oracle: ClassLoader (Java Plattform SE 6). API-Dokumentation. URL: http://download.oracle.com/javase/6/docs/api/java/lang/ClassLoader.html [Stand: 21.01.11]