JVM und JDK unter der Haube

Table of Contents

Woher lädt Java eigentlich seine JDK Klassen?

Beim Start einer Applikation kann mit -cp ein Klassenpfad angegeben werden, in welchem sich Klassen befinden, die man verwenden möchte. Das ist das normale Vorgehen, wie man Klassen integriert. Doch woher hat das JDK seine eigenen Klassen?

Um das zu verstehen müssen wir uns zuerst mit dem Classloader befassen. Classloader sind selbst Java-Objekte mit denen man interagieren kann. Es gibt grundsätzlich mindestens drei Classloader:

  • Bootstrap-Classloader: lädt die JDK-Klassen
  • Extension-Classloader: lädt Klassen, die in Javas Erweiterungsverzeichnis liegen. In diesem selten genutzten Verzeichnis können Erweiterungen des JDKs abgelegt werden, die jeder Anwendung zur Verfügung stehen, ohne dass sie sie in den eigenen Klassenpfad aufnehmen müsste. Der Default ist das Erweiterungsverzeichnis im Java-Installationsverzeichnis unter lib/ext zu finden.
  • System-Classpath-Classloader: Der Classloader der Anwendung. Er lädt die Klassen welche mit -cp im angegebenen Verzeichnis liegen.

Kann man durch eine eigene java.lang.Integer Klasse die Integer JDK Klasse ersetzen?

Diese drei Klassloader stehen in einer hierarchischen Beziehung miteinander. Wenn man nun in seiner Applikation Klassen lädt, geschieht das immer mit dem System-Classpath-Classloader. Im ersten Schritt fragt jeder Classloader bei seinem übergeordneten Classsloader nach, ob dieser die Klasse laden kann. Diese Reihenfolge ist extrem wichtig, denn sie privilegiert die JDK Klassen. Eine Klasse, die dem Bootstrap-Classloader bekannt ist, kann deshalb niemals durch eine andere Klasse desselben Namens verdrängt werden. Etwas komplizierter ist das bei den Serverlet-Container.

Garbage Collection

Jedes erzeugte Java-Objekt belegt Speicher und der JVM steht nur eine begrenzte Speichermenge zur Verfügung. Es muss also eine Möglichkeit geben, Objekte wieder aus dem Speicher zu entfernen, wenn diese nicht mehr benötigt werden. Ansonsten wird jede Java-Anwendung irgendwann mit einem OutOfMemoryError beendet. Dieser Mechanismus nennt sich Garbage Collection.

Andere systemnähere Programmiersprachen haben keinen solchen Mechanismus und überlassen diese Aufgabe dem Entwickler selbst. Viele moderne Sprachen bieten mittlerweile eine automatische Garbage Collection an, aber Java war eine der ersten mit diesem Feature.

Welche Objekte nicht mehr verwendet werden, findet der Garbage Collector heraus, indem er Referenzen untersucht. Dazu geht er von den Garbage Collection Roots aus und folgt allen Referenzen von diesen Objekten aus. Alle Objekte, die von einer Root referenziert werden, werden als noch aktiv markiert. Objekte, die von diesen Objekten aus referenziert werden, werden ebenfalls markiert usw. Garbage Collection Roots können Class-Objekte, lebende Threads, Lokale Variablen und Parameter, Monitor-Objekte, JNI-Variablen und spezielle JVM-Objekte sein. Rekursiv werden so alle Objekte markiert, die von einer Garbage Collection Root direkt oder indirekt erreichbar sind. Anschliessend werden alle Objekte die nicht von einer Root aus erreichbar sind aus dem Speichern entfernt. Dieses Vorgehen nennt sich Mark and Sweep Collection. Die echten Abläufe der Garbage Collection sind noch etwas komplexer als hier beschrieben.

Speicherlecks

Speicherlecks in Java kommen fast immer daher, dass man Objekte mit einer der Garbage Collection Roots verbinden und nicht wieder von dort entfernen. Am häufigsten passiert das mit statischen Variablen. Es ist leicht zu vergessen, dass ihr Inhalt nicht von selbst aus dem Speicher entfernt wird, im Unterschied zu Instanzvariablen. So kann man unbeabsichtigt viel Speicher belegen, indem man ein grosses Objekt in einer statischen Variablen hält, obwohl es bereits nicht mehr benötigt wird. Dabei handelt es sich aber nicht um ein Speicherleck, denn der belegte Speicher bleibt konstant und wächst mit der Zeit nicht mehr an. Ein echtes Speicherleck kann dadurch entstehen, dass man eine Collection in einem statischen Feld hält, der immer weiter Objekte hinzugefügt werden:

public class Speicherleck(){
    private static final Map<String, Speicherleck> cache = new HashMap<>();
    private String name;

    public Speicherleck(String name){
        this.name = name;
        cache.put(name, this);
    }
}

Jede Instanz der Klasse Speicherleck, die jemals erzeugt wird, wird in der statischen map cache gespeichert, aber nie wieder von dort entfernt. Das führt dazu, dass keine Instanz von Speicherleck jemals vom Garbage Collector entsorg werden kann, denn alle Instanzen werden von einem statischen Feld referenziert. Es kann natürlich sein, dass Sie diese Objekte wirklich noch brauchen, aber häufiger handelt es sich bei solchen Konstrukten um einen Fehler.

Weitere Quellen für Speicherlecks in Java sind ThreadLocals, Variablen, die direkt mit einem Thread verbunden sind. Die genauen Mechanismen, wie es zu einem Speicherleck kommen kann, sind sehr ähnlich wie bei statischen Feldern: Solange der Thread lebendig ist, kann der Inhalt eines ThreadLocal nicht aus dem Speicher entfernt werden. Eine wachsende Collection dort hat deshalb denselben zerstörerischen Effekt wie in einem Statischen Feld.