Lebenszyklus von Objekten und Klassen


[ Seminar "Java und Werkzeuge für das Web" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ... [ Literaturverzeichnis ]

Übersicht: Lebenszyklus


Überblick

Der Lebenszyklus einer Klasse lässt sich wie folgt gliedern: Wurde eine Klasse geladen, so können Instanzen von ihr erzeugt (Instanziierung) und wieder freigegeben (Finalisierung) werden.

Laden

Beim Laden einer Klasse muss der verantwortliche Classloader (entweder der Bootstrap-Classloader oder ein benutzerdefinierter) zu einem gegebenen, voll qualifizierten Klassennamen, die zugehörigen binären Daten finden und in die Method Area laden. Wie bereits weiter oben gesagt, kann jeder CL dafür seine eigene Methode implementieren, wie Klassen zu lesen sind. Es steht Classloadern (auch dem Bootstrap-CL) frei, neben dem Java-class-Format auch andere Dateiformate zu erkennen und zu laden.
Außerdem wird in diesem Schritt eine Instanz von java.lang.Class erzeugt, die die geladene Klasse repräsentiert.

Linken

Verifizieren

"Verifizieren" bedeutet, dass die geladenen Daten auf ihre Korrektheit überprüft werden müssen. Dies muss nicht zwangläufig während des Linkens passieren, sondern kann auch (ganz oder teilweise) schon während des Ladens oder erst während des Initialisierens stattfinden.
Die Spezifikation der VM legt genau fest, in welchen Fehlerfällen welche Exceptions bzw. Errors geworfen werden müssen.
Beispiele für notwendige Prüfungen sind:

Vorbereiten

In der Vorbereitungs-Phase wird der Speicher für Klassenvariablen bereitgestellt und mit Nullwerten (0, null, 0.0, false) initialisiert.
Außerdem wird hier der Speicher für implementationsabhängige Datenstrukturen bereitgestellt. Ein Beispiel für eine solche Datenstruktur ist eine Methodentabelle, mit deren Hilfe einer VM schnellerer Zugriff auf geerbte Methoden ermöglicht wird.

Auflösen

"Auflösen" (Resolution) bedeutet, dass die symbolischen Verweise auf Klassen, Methoden und Felder im Konstantenpool durch Referenzen auf die tatsächlichen Klassen ersetzt werden.
Beim Laden schreibt der Classloader in den Konstantenpool für alle Referenzen auf fremde Klassen den voll qualifizierten Klassen-/Methoden-/Feldnamen. In der Auflösungs-Phase muss jeder dieser Einträge durch einen Zeiger auf den entsprechenden Eintrag in der Method Area ersetzt werden. Dabei muss außerdem die Prüfung vorgenommen werden, ob der referenzierte Eintrag auch wirklich existiert und auch von der aktuellen Klasse/Methode referenziert werden darf.
Das Auflösen symbolischer Verweise kann gleich bei Erzeugung der Klasse (early resolution) oder auch erst beim ersten Zugriff auf den Verweis (late resolution) passieren. Die VM darf jedoch Fehler, auf die sie stößt, erst beim ersten Zugriff werfen. Das bedeutet, wenn die jeweilige Klasse/Methode nicht verwendet wird, wird der Fehler auch nicht angezeigt. Nach außen hin muss es die VM also den Eindruck erwecken, nach dem Prinzip der late resolution zu arbeiten.

Initialisierung

Die Initialisierung eines Typs muss genau dann stattfinden, wenn ein Typ das erste Mal verwendet wird. "Verwenden" bedeutet hierbei: In dieser Phase werden statische Variablen mit ihren angegebenen Startwerten initialisiert und statische Initialisierer ausgeführt. Dabei wird zuerst die Elternklasse – wenn vorhanden – initialisiert (wenn das noch nicht passiert ist), der Typ selbst.
Der Java-Compiler sammelt sämtliche Initialisierungen (Klassenvariablen-Initialisierer und statische Initialisierer) in einer Methode namens <clinit>() (einschließlich der spitzen Klammern - diese Namenskonvention stellt sicher, dass es keine benutzerdefinierte Methode dieses Namens gibt, da ein solcher Methodenname für die Programmiersprache Java ungültig ist), die bei der Initialisierung ausgeführt wird - und zwar in der Reihenfolge, in der sie definiert sind. Hierzu ein Beispiel:
class Beispiel {

    static int a;
    static int b;
    static int c = 42;
    final static int d=99;

    // statischer Initialisierer:
    static {

       b = 37;

    }
}
Für diese Klasse würde der Compiler für die Klasse Example eine Methode <clinit>() erzeugen, die (decompiliert) in etwa folgendes Aussehen hätte:
static void <clinit>() {

    c = 42;
    b = 37;

    }
a wird in dieser Methode nicht initialisiert, da die Initialisierung mit Nullwerten bereits in der Vorbereitungsphase stattgefunden hat. d ist als final static deklariert, und wird deshalb vom Compiler als Konstante und nicht als statische Variable behandelt.

Instanziierung von Objekten

Sind die obigen Punkte abgearbeitet, so können ab jetzt Instanzen des geladenen Typs erzeugt werden. Instanziierung erfolgt, sobald ein Objekt (durch new(), durch newInstance() des zugehörigen Class-Objekts, o.ä.) erzeugt wird.
Dabei alloziert die VM den nötigen Speicher für die Instanzvariablen des Objekts auf dem Heap (und zwar für die Variablen der Klasse selbst und sämtlicher Elternklassen) und initialisiert sie mit Nullwerten.
Anschließend wird die Methode <init>() aufgerufen. Diese wird vom Compiler analog zu <clinit>() erzeugt und enthält die Vorbelegungen für Instanzvariablen. Dieser Methodenaufruf erfolgt nicht, wenn das Objekt durch clone() oder durch Lesen aus einem Stream erzeugt wurde, da dann die Instanzvariablen bereits festgelegt sind.

Finalisierung

Bekanntermaßen bietet die Programmiersprache Java keine Möglichkeit, Speicherbereiche explizit freizugeben. Diese Aufgabe erfüllt die so genannte Garbage Collection: Dieser Prozess soll erkennen, wenn Klassen und/oder Objekte nicht mehr referenziert werden, und ihren Speicherbereich wieder zur Verfügung stellen. Garbage Collection ist allerdings kein Bestandteil der VM-Spezifikation: Auch ein Programm, das Java-Programme ausführt und mit einer Fehlermeldung abbricht, sobald kein Speicher mehr vorhanden ist - ohne jemals den Versuch zu machen, nicht mehr benötigten Speicher zu identifizieren und freizugeben - ist eine gültige VM.
Hat eine VM jedoch die Möglichkeit, Speicher freizugeben (mit Hilfe von Garbage Collection oder auch durch einen anderen Prozess), so muss sie vorher vom freizugebenden Objekt die Methode finalize() (wenn vorhanden) aufrufen. In dieser Methode kann der Programmierer Code angeben, der vor der Speicherfreigabe auf jeden Fall auszuführen ist. Theoretisch ist es auch möglich, in dieser Methode eine neue Referenz auf das Objekt zu schaffen, so dass eine Freigabe danach nicht mehr möglich ist.
Es ist jedoch nicht definiert, wann (oder auch nur ob) dieser Methodenaufruf stattfindet. Garbage Collection ist erstens "optional" (muss also nicht in jeder denkbaren VM-Implementierung existieren), und zweitens muss sie auch nicht zeitnah sein (das heißt, es ist nicht festgelegt, wie lange nachdem ein Objekt nicht mehr referenziert wird sein Speicher freigegeben wird).
[ Seminar "Java und Werkzeuge für das Web" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ oben ] ... [ weiter ] ... [ Literatur ]