8.4 Klassenlader (Class Loader)
8.4.1 Woher die kleinen Klassen kommen
Nehmen wir zu Beginn ein einfaches Programm mit zwei Klassen her:
class A
{
static String s = new java.util.Date().toString();
public static void main( String args[] )
{
B b = new B();
}
}
class B
{
A a;
}
Wenn nun Programm A gestartet wird, wird die Laufzeitumgebung einige Klassen laden müssen. Sofort wird klar, dass es zumindest A sein muss. Wenn aber die main()-Funktion aufgerufen wird, muss auch B geladen sein. Und da beim Laden einer Klasse auch die statischen Variablen initialisiert werden, wird auch die Klasse java.util.Date geladen. Zwei weitere Dinge werden nach einiger Überlegung deutlich:
|
Wenn B geladen wird, bezieht es sich auf A. Da A aber schon geladen ist, muss es nicht noch einmal geladen werden. |
|
Unsichtbar stecken noch andere referenzierte Klassen dahinter, die nicht direkt sichtbar sind. So wird zum Beispiel Object geladen werden, da implizit in der Klassendefinition von A steht: class A extends Object. |
Um zu sehen, welche Klassen überhaupt geladen werden, lässt sich der virtuellen Maschine beim Start der Laufzeitumgebung ein Schalter mitgeben: -verbose. Dann gibt die Maschine beim Lauf alle Klassen aus, die sie lädt.
Die Suchorte
Die Suche nach den Klassen bestimmt ein festes Schema aus drei Stufen:
|
Klassen wie String, Object oder Point stehen in einem ganz speziellen Archiv. Wenn ein eigenes Java-Programm gestartet wird, so sucht die virtuelle Maschine die angeforderten Klassen zuerst in diesem Archiv. Da es elementare Klassen sind, die zum hochfahren eines Systems gehören, werden sie Bootstrap-Klassen genannt. Das Archiv mit diesen Klassen heißt oft rt.jar (für Run-Time). Zusätzlich können andere Archive dazukommen wie i18n.jar, in dem Internationalisierungsdaten stehen. |
|
Findet die Laufzeitumgebung die Klassen nicht bei den Bootstrap-Klassen, so werden alle Archive eines speziellen Verzeichnisses untersucht, das sich Extension-Verzeichnis nennt. Das Verzeichnis gibt es bei jeder Java-Version und liegt unter lib/ext. Werden hier Klassen eingelagert, so findet es die Laufzeitumgebung ohne weitere Anpassung und setzen von Pfaden. In sonstigen Verzeichnissen einer Java-Installation sollten keine Klassen kopiert werden. |
|
Ist eine Klasse auch im Erweiterungsverzeichnis nicht zu finden, beginnt die Suche im Klassen-Pfad (Classpath). Dieser Pfad ist eine Angabe von einzelnen Klassen oder Jar-Archiven, in der die Laufzeitumgebung nach den Klassendateien sucht. Standardmäßig ist dieser Classpath auf das aktuelle Verzeichnis - also . gesetzt; doch kann er natürlich erweitert werden. Dazu gibt es drei Varianten: Setzen einer Umgebungsvariaben java.class.path mit einer Zeichenfolge, die die Klassen spezifiziert, oder aufführen der Klassen über den Schalter -classpath (kurz -cp) beim Start der virtuellen Maschine. |
java -classpath classpath1;classpath2
set CLASSPATH=classpath1;classpath2
Hinweis Früher - das heißt vor Java 1.2 - umfasste der CLASSPATH auch die Bootstrap-Klassen. Das ist seit 1.2 überflüssig.
|
8.4.2 Drei Typen von Klassenladern
Eine Klassendatei kann von der Java-Laufzeitumgebung über drei verschiedene Klassenlader bezogen werden. Es gibt System-, Erweiterungs- und Applikations-Klassenlader. Sie arbeiten insofern zusammen, als das sie sich die Aufgaben zuschieben, wenn eine Klassen nicht gefunden wird.
|
System-Klassenlader: für die Bootstrap-Klassen. |
|
Erweiterungs-Klassenlader: für die Klassen im lib/ext-Verzeichnis. |
|
Applikations-Klassenlader: Der letzte Klassenlader im Bunde ist ein eigener, den man zum Laden bilden kann. |
Aus Sicherheitsgründen beginnt der Klassenlader bei einer neuen Klasse immer mit dem System-Klassenlader und reicht dann die Anfrage weiter, wenn er selbst die Klasse nicht laden konnte. Dazu sind die Klassenlader miteinander verbunden. Jeder Klassenlader L hat dazu einen Vater-Klassenlader V. Erst darf der Vater versuchen, die Klassen zu laden. Kann er es nicht, gibt er die Arbeit an L ab.
8.4.3 Der java.lang.ClassLoader
Der Applikations-Klassenlader in Java ist der ClassLoader im Kernpaket java.lang. Er hat eine loadClass()-Funktion, mit dem Klassen eingelesen und angemeldet werden. Wird loadClass() auf einer Klasse aufgerufen, die dieser Klassenlader schon eingelesen hat, so kehrt die Funktion zurück. Kennt sie die Klasse nicht, darf zuerst der Vater versuchen die Klasse zu laden; findet er die Klasse nicht, so darf der Klassenlader selbst heran und mit findClass() wird selbst versucht, die Klasse zu beziehen. Eigene Klassenlader überschreiben die Methode, um selbst nach einem bestimmten Schema zu suchen, etwa nach Klassen aus der Datenbank. Wie ein eigener Klassenlader aussieht, zeigt das Beispiel unter http://www.ibiblio.org/javafaq/books/jnp/javanetexamples/10/URLClassLoader.java. Hier wird ein URL-ClassLoader noch einmal nachimplementiert. Im Fall eines Servlets-Container überwacht dieser Klassen und lädt sie bei Änderungen neu. So etwas gibt natürlich schon und die API-Dokumentation eines solchen »adaptiven Klassenladers« lässt sich bei Apache unter http://java.apache.org/jserv/api/org/apache/java/lang/AdaptiveClassLoader.html finden.
8.4.4 Hot Deployment mit dem URL-ClassLoader
Unter »Hot Deployment« ist die Möglichkeit zu verstehen, zur Laufzeit Klassen auszutauschen. Diese Möglichkeit ist für viele EJB- oder Servlet-Container wichtig, denn sie lauschen im Dateisystem auf eine neue Klassendatei und ersetzen dann die alte Klasse durch eine neue.
Damit dieser heiße Wechsel funktioniert, muss die Klasse über einen neuen Klassenlader bezogen werden. Das liegt daran, dass der Standardklassenlader von Haus aus keine Klasse mehr loswird, wenn er sie einmal geladen hat. Mit anderen Worten: Wenn eine Klasse über Class.forName(Klasse) angefordert wird, ist sie immer im Cache und sie wird nicht mehr entladen. Ein neuer Klassenlader fängt immer von vorne an, wenn er die Klasse für sich zum ersten Mal sieht.
Mit immer neuen Klassenladern funktioniert das Neuladen, denn dann ist jeweils ein Klassenlader für eine neue Klasse zuständig. Ändert sich die Klasse, so wird ein neuer Klassenlader konstruiert, der die neue Klasse lädt. Damit ist die alte Klasse noch nicht aus dem Spiel, nur wenn sich niemand mehr für die alte Klasse interessiert und für den Klassenlader auch nicht, dann kann die Laufzeitumgebung diese nicht benutzen Objekte erkennen und aufräumen.
Wir wollen im Folgenden eine Funktion neuesExemplar() vorstellen, die bei Aufruf die neuste Version vom Dateisystem lädt und ein Exemplar bildet. Die Klasse, die neu geladen werden soll, soll o.b.d.A einen Standardkonstruktor haben - andernfalls muss über Reflection ein parametrisierter Konstuktor aufgerufen werden.
public class NeuZuLadeneKlasse
{
static
{
System.out.println( "NeuZuLadeneKlasse" );
}
}
Jetzt brauchen wir noch eine Testklasse, die NeuZuLadeneKlasse unter dem Wurzelverzeichnis liest (also etwa unter c:/):
import java.io.File;
import java.net.*;
public class KlassenNeuladen
{
static Object neuesExemplar( String pfad, String klassename ) throws Exception
{
URL url = new File( pfad ).toURL();
URLClassLoader cl = new URLClassLoader( new URL[]{ url } );
Class c = cl.loadClass( klassename );
return c.newInstance();
}
public static void main( String args[] ) throws Exception
{
neuesExemplar( "/", "NeuZuLadeneKlasse" );
neuesExemplar( "/", "NeuZuLadeneKlasse" );
}
}
Zweimal werden wir nun Zeuge, wie die virtuelle Maschine auf der Konsole die Meldung ausgibt, wenn der statische Initialisierungsblock ausgeführt wird.
Die Klasse, die geladen werden soll, darf nun aber keine Standardklasse (etwa aus java.lang) sein. Das liegt daran, dass auch in dem Fall, in dem die Klasse mit dem eigenen URLClassLoader bezogen werden soll, trotzdem erst die Anfrage an den System-Klassenlader, dann an den Erweiterungs-Klassenlader und erst ganz zum Schluss an unseren eigenen geht. Es ist also aus einem Java-Programm nicht möglich, Klassen zu beziehen, die prinzipiell vom System-Klassenlader geladen werden. Wir können also eine Klasse wie javax.swing.JButton nicht selbst beziehen. Wenn er natürlich mit einem Klassenlader ungleich unserem eigenen geladen wird, hat das wiederum zum Folge, dass wir die geladene Klasse nicht mehr loswerden - was allerdings im Fall der Systemklassen kein Problem sein sollte. Wichtig ist hier, dass die Klasse NeuZuLadeneKlasse nicht vom Standardklassenlader sichtbar sein darf. Wir müssen die Klasse also zum Beispiel aus dem Pfad löschen, denn andernfalls kommt aufgrund des niedrigen Rangs unser einer URL-Klassenlader nicht zum Zuge.
8.4.5 Das jre/lib/endorsed-Verzeichnis
Im Fall der XML-Parser und Bibliotheken kommt es häufiger vor, dass sich die Versionen einmal ändern. Es wäre nun müßig, aus diesem Grund die neuen Bibliotheken, immer im bootclasspath aufzunehmen, da dann immer eine Einstellung über die Kommandozeile stattfinden würde. Die Entwickler haben daher für spezielle Pakete ein Verzeichnis vorgesehen, in denen Updates eingelagert werden können. Das ist das Verzeichnis jre/lib/endorsed der Java-Installation. Alternativ können die Klassen und Archive auch durch die Kommandozeilenoption java.endorsed.dirs spezifiziert werden.
Wenn im endorsed-Verzeichnis eine neue Version - etwa vom XML-Parser Xalan - gefunden wird, lädt der Klassenlader die Klassen von dort und nicht aus dem Jar-Archiv, aus dem sonst die Klassen geladen würde, also in der Regal aus rt.jar. Alle gefunden Klassen überdecken und ergänzen (engl. endorse) somit die Standardklassen aus der J2SE und neue Versionen lassen sich einfach einspielen.
Nicht alle Klassen lassen sich mit endorsed überdecken. Es lässt sich zum Beispiel keine neue Version von java.lang.String einfügen. Die Dokumentation »Endorsed Standards Override Mechanism« zeigt die überschreibbaren Pakete an: javax.rmi.CORBA, org.omg.*, org.w3c.dom, org.xml.*. (Im Übrigen definiert auch Tomcat, der Servlet-Engine, ein solches Überschreib-Verzeichnis. Hier lassen sich Klassen in das common/lib/endorsed-Verzeichnis aufnehmen, die dann beim Start von Tomcat die Standardklassen überschreiben.)
8.4.6 Wie heißt die Klasse mit der Methode main()?
In C(++) ist das erste Element des Felds der Funktion main(int argc, char **argv) der Name des Programms. Das ist in Java nicht so. Die Methode enthält als ersten Parameter nicht den Namen der Klasse beziehungsweise des Programms, sondern einfach den ersten Parameter, sofern auf der Kommandozeile übergeben. Mit einem kleinen Umweg geht das auch für Klassen.
Der zu einer Klasse gehörende Klassenlader lässt sich mit dem Class-Objekt erfragen. Mit der Methode getResource() bekommen wir von einem Klassennamen ein URL-Objekt zurück, welches dann die Position der Klassendatei im Dateisystem anzeigt. Das folgende Programmbeispiel zeigt, wie wir von einer Klasse den vollständigen Dateipfad zurückbekommen.
Es funktioniert allerdings nur für Klassen, die nicht in einem Jar-Archiv eingebunden sind und nicht aus den Standardbibliotheken kommen. Auch ist eine Dateiangabe unmöglich, wenn wir etwa einen eigenen Klassenlader schreiben, der die Klassen aus einer Datenbank nimmt. Dann gibt es keinen Pfad mehr.
Listing 8.4 FindClassPath.java
import java.net.*;
class FindClassPath
{
static String getClassPath( Class clazz )
{
ClassLoader loader = clazz.getClassLoader();
if ( loader == null )
return null;
URL url = loader.getResource(clazz.getName().replace('.','/')
+ ".class");
return ( url != null ) ? url.toString() : null;
}
public static void main( String args[] ) throws Exception
{
Class c = Class.forName( "FindClassPath" );
System.out.println( "Klasse: " + c.getName() );
System.out.println( "Dateiname: " + getClassPath(c) );
}
}
Benötigen wir den Ort einer Klasse, um mit dieser Information auf weitere Dateien im Verzeichnis zuzugreifen, geht es mit der Class.getResourceAsStream(String) einfacher. Diese Methode dient insbesondere dazu, Ressourcen wie Bilder oder Musik aus einem Jar-Archiv auszulesen. Auch der ClassLoader bietet die Methode getResourceAsStream(String) an. Diese Methoden funktionieren ebenfalls für Klassen aus Jar-Archiven, wenn die Ressource auch im Archiv liegt.
|