Lebenszyklus von Objekten und Klassen
[ Seminar "Java und Werkzeuge für das Web" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ... [ Literaturverzeichnis ]
Übersicht: Lebenszyklus
Der Lebenszyklus einer Klasse lässt sich wie folgt gliedern:
- Laden
- Linken
- Verifizierung
- Vorbereitung
- Auflösung
- Initialisieren
Wurde eine Klasse geladen, so können Instanzen von ihr erzeugt (Instanziierung) und wieder freigegeben (Finalisierung) werden.
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.
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:
Final
-Klassen dürfen keine Nachkommen haben, Final
-Methoden nicht überschrieben werden
- Verweise innerhalb des Konstantenpools müssen auf passende Einträge zeigen (mehr zum Konstantenpool siehe Kapitel 5).
- Einträge, die sich auf Klassen-, Feld- oder Methodennamen beziehen, müssen wohlgeformt sein, das heißt, den Namensgebungskonventionen von Java gehorchen
- Der Bytecode muss korrekt sein
Dieser letzte Punkt ist sicherlich am schwierigsten zu überprüfen. Notwendige Tests sind in diesem Berich zum Beispiel:
- Die Instruktinen müssen definiert sein (Das Java-Bytecode-Format nutzt (zur Zeit) nicht alle 256 theoretisch möglichen Instruktionen aus)
Jump
-Instruktionen müssen auf einen Punkt innerhalb der aktuellen Methode verweisen
- Zuweisungs- und Vergleichsoperationen müssen zwischen kompatiblen Typen stattfinden (kein
int
einem String
zuweisen, o.ä.)
Diese Prüfungen müssen ebenfalls nicht während des Linkens stattfinden. Die Spezifikation gestattet es, sie erst vorzunehmen, wenn der entsprechende Bytecode ausgeführt wird. Jedoch wurde der Java-Bytecode so gestaltet, dass er mit Hilfe eines Datenfluss-Analyzers komplett zu überprüfen ist. Wird die Prüfung bereits während des Linkens vorgenommen, so hat dies den Vorteil, dass das Programm schneller ablaufen kann, da die entsprechenden Prüfungen nicht mehr zur Laufzeit stattfinden müssen.
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.
Die Initialisierung eines Typs muss genau dann stattfinden, wenn ein Typ das erste Mal verwendet wird. "Verwenden" bedeutet hierbei:
- Erzeugung eines Objekts dieses Typs durch Aufruf von
new
- Verwendung oder Wertzuweisung einer statischen Methode oder eines nichtfinalen statischen Feldes
- Verwendung bestimmter Methoden aus der Reflection-API
- Initialisierung einer Unterklasse der gegebenen Klasse
- Aufruf der
main()
– Methode der Klasse
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.
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.
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 ]