6.5 Objekte anlegen und zerstören
Legt ein Programm Objekte mit dem Schlüsselwort new an, reserviert die Speicherverwaltung des Laufzeitsystems auf dem System-Heap Speicher. Anschließend werden die Objektzustände initialisiert. Wird das Objekt nicht mehr referenziert, so räumt die automatische Speicherbereinigung (Garbage-Collector) in bestimmten Abständen auf und gibt den Speicher an das Laufzeitsystem zurück.
6.5.1 Konstruktoren schreiben
Legt ein Programm mit new ein neues Objekt an, so wird zur Initialisierung automatisch ein Konstruktor aufgerufen. Jede Klasse muss in Java einen Konstruktor haben. Entweder erzeugt der Compiler automatisch einen Konstruktor, oder wir legen einen eigenen an. Mit einem eigenen Konstruktor erreichen wir, dass ein Objekt nach seiner Erzeugung einen benutzerdefinierten Anfangszustand aufweist. Dies kann bei Klassen, die Variablen beinhalten, notwendig sein, weil sie ohne vorherige Zuweisung bzw. Initialisierung keinen Sinn ergäben.
Konstruktordeklarationen
Konstruktordeklarationen enthalten besondere Initialisierungen und sehen ähnlich wie Methodendeklarationen aus – so gibt es auch die bekannten Sichtbarkeiten, Parameterlisten und Überladung –, doch es bestehen zwei deutliche Unterschiede:
-
Konstruktoren tragen immer denselben Namen wie die Klasse.
-
Konstruktordeklarationen besitzen keinen Rückgabetyp, also noch nicht einmal void.
[zB] Beispiel
Soll eine Klasse Candy einen Konstruktor bekommen, schreiben wir:
class Candy {
Candy() { } // Konstruktor der Klasse Candy
}
Ein Konstruktor, der keinen Parameter besitzt, nennt sich parameterloser Konstruktor oder auch auf Englisch no-arg constructor oder nullary constructor.
Aufrufreihenfolge
Dass der Konstruktor während der Initialisierung und damit vor einem äußeren Methodenaufruf aufgerufen wird, soll ein kleines Beispiel zeigen:
class Candy {
Candy() {
System.out.println( "2. Im Konstruktor" );
}
void eat() {
System.out.println( "4. Essen" );
}
public static void main( String[] args ) {
System.out.println( "1. Vor dem Konstruktor" );
Candy d = new Candy ();
System.out.println( "3. Nach dem Konstruktor" );
d.eat();
System.out.println( "5. Bauchweh" );
}
}
Die Aufrufreihenfolge auf dem Bildschirm ist:
1. Vor dem Konstruktor
2. Im Konstruktor
3. Nach dem Konstruktor
4. Essen
5. Bauchweh
[»] Hinweis
UML kennt zwar Attribute und Operationen, aber keine Konstruktoren im Java-Sinne. In einem UML-Diagramm werden Konstruktoren wie Operationen gekennzeichnet, die eben nur so heißen wie die Klasse.
6.5.2 Verwandtschaft von Methode und Konstruktor
Methoden und Konstruktoren besitzen eine Reihe von Gemeinsamkeiten:
-
Sie haben Programmcode.
-
Sie haben eine (optionale) Parameterliste.
-
Sie können Modifizierer tragen.
-
Sie können auf Objektvariablen zugreifen und this verwenden.
Ein schon erwähnter Unterschied ist, dass Methoden einen Rückgabetyp besitzen (auch wenn er nur void ist), Konstruktoren aber nicht. Zwei weitere Unterschiede betreffen die Syntax und Semantik.
Konstruktoren tragen immer den Namen ihrer Klasse, und da Klassennamen per Konvention großgeschrieben werden, sind auch Konstruktoren immer großgeschrieben – Methoden werden in der Regel immer kleingeschrieben. Und Methoden sind in der Regel Verben, die das Objekt anweisen, etwas zu tun; Klassennamen sind Nomen und keine Verben.
Der Programmcode eines Konstruktors wird automatisch nach dem Erzeugen eines Objekts von der JVM genau einmal, und zwar als Erstes vor allen anderen Methoden aufgerufen. Methoden lassen sich beliebig oft aufrufen und unterliegen der Kontrolle des Benutzers. Konstruktoren lassen sich später nicht noch einmal auf einem schon existierenden Objekt aufrufen und so ein Objekt reinitialisieren. Der Konstruktoraufruf ist implizit und automatisch mit new verbunden und kann nicht getrennt vom new gesehen werden.
Zusammenfassend können wir sagen, dass ein Konstruktor eine Art spezielle Methode zur Initialisierung eines Objektes ist.
6.5.3 Der Standard-Konstruktor (default constructor)
Wenn wir in unserer Klasse überhaupt keinen Konstruktor angeben, legt der Compiler automatisch einen an, da es immer einen Konstruktor geben muss. Diesen Konstruktor nennt die Java-Sprachdefinition (JLS) default constructor, was wir im Deutschen Standard-Konstruktor nennen wollen.
Schreiben wir
class Candy { }
dann macht der Compiler daraus eine Version, die im Bytecode identisch ist mit:
class Candy {
Candy() { }
}
Der vorgegebene Konstruktor hat immer die gleiche Sichtbarkeit wie die Klasse. Ist die Klasse paketsichtbar, ist es auch der Konstruktor. Und setzen die Modifizierer public/private/protected[ 153 ](Nur innere Typen können private oder protected sein. ) die Typsichtbarkeit, wird auch der automatisch eingeführte Konstruktor public/private/protected sein.
Standard-Konstruktor oder parameterloser Konstruktor
Ob ein parameterloser Konstruktor vom Compiler oder Entwickler angelegt wurde, ist ein Implementierungsdetail, das für Nutzer der Klasse irrelevant ist. Daher ist es im Grunde egal, ob wir einen parameterlosen Konstruktor selbst anlegen oder ob wir uns einen vorgegebenen Konstruktor vom Compiler generieren lassen: Im Bytecode lässt sich das nicht mehr unterscheiden, und auch für den Nutzer der Klasse ist es irrelevant. Selbst eine generierte Javadoc-API-Dokumentation von einer public class C1 {} und public class C2 { public C2(){} } wäre strukturell gleich.
Der Standard-Konstruktor ist also immer ein parameterloser Konstruktor. Auch wenn der Compiler einen Standard-Konstruktor anlegt, ist es oft sinnvoll, einen eigenen parameterlosen Konstruktor anzugeben, auch wenn der Rumpf leer ist. Ein Grund ist, ihn mit Javadoc zu dokumentieren, ein anderer Grund ist, die Sichtbarkeit explizit zu wählen, etwa wenn die Klasse public ist, aber der Konstruktor privat sein soll. Dazu folgt gleich mehr.
Private Konstruktoren
Ein Konstruktor kann privat sein, was verhindert, dass von außen ein Exemplar dieser Klasse gebildet werden kann. Was auf den ersten Blick ziemlich beschränkt erscheint, erweist sich als ziemlich clever, wenn damit die Exemplarbildung bewusst verhindert werden soll. Sinnvoll ist das etwa bei den sogenannten Utility-Klassen. Das sind Klassen, die nur statische Methoden besitzen, also Hilfsklassen sind. Beispiele für diese Hilfsklassen gibt es zur Genüge, zum Beispiel Math. Warum sollte es hier Exemplare geben? Für den Aufruf von max(…) ist das nicht nötig. Also wird die Bildung von Objekten erfolgreich mit einem privaten Konstruktor unterbunden.
6.5.4 Parametrisierte und überladene Konstruktoren
Der Standard-Konstruktor hatte keine Parameter, und daher hatten wir ihn auch parameterlosen Konstruktor genannt. Ein Konstruktor kann aber wie eine Methode auch eine Parameterliste besitzen: Er heißt dann parametrisierter Konstruktor oder allgemeiner Konstruktor. Konstruktoren können wie Methoden überladen, also mit unterschiedlichen Parameterlisten deklariert sein.
Folgende Anforderung wollen wir umsetzen: Ein City-Objekt muss immer mit einem Namen erzeugt werden. Optional lässt sich eine Anzahl Einwohner mit übergeben:
public class City {
private String name;
private int population;
public City( String name ) {
setName( name );
}
public City( String name, int population ) {
setName( name );
setPopulation( population );
}
public void setName( String name ) {
this.name = Objects.requireNonNull( name );
}
public String getName() {
return name;
}
public void setPopulation( int population ) {
if ( population >= 0 )
this.population = population;
}
public int getPopulation() {
return population;
}
}
Die Nutzung kann so aussehen:
City kandy = new City( "Kandy" );
System.out.printf( "%s %d%n",
kandy.getName(), kandy.getPopulation() ); // Kandy 0
City hershey = new City( "Hershey", 10_200 );
System.out.printf( "%s %d%n",
hershey.getName(), hershey.getPopulation() ); // Hershey 10200
Die parametrisierten Konstruktoren verbinden sozusagen die Initialisierung mit den Settern. Die Objektvariablen könnten direkt belegt werden, doch wir leiten die zu setzenden Werte an die Setter weiter, denn so kann der Setter gleich validieren, und der Konstruktor ist von der Validierungsaufgabe befreit – wie die Regel so schön lautet: Don’t repeat yourself!
Wann fügt der Compiler keinen vorgegebenen Konstruktor ein?
Wenn es mindestens einen ausprogrammierten Konstruktor gibt, erzeugt der Compiler keinen Standard-Konstruktor mehr. Wenn wir also nur parametrisierte Konstruktoren haben – wie in unserem obigen Beispiel –, führt der Versuch, bei unserer Städteklasse ein Objekt einfach mit dem parameterlosen Konstruktor über new City() zu erzeugen, zu einem Compilerfehler, da es eben keinen vom Compiler generierten Standard-Konstruktor gibt:
City city = new City(); // Cannot resolve constructor 'City()'
Dass der Compiler keinen vorgegebenen Konstruktor anlegt, hat seinen guten Grund: Es ließe sich sonst ein Objekt anlegen, ohne dass vielleicht wichtige Variablen initialisiert worden wären. So ist das bei unserer Stadt. Die parametrisierten Konstruktoren erzwingen, dass beim Erzeugen ein Städtename angegeben wird, sodass nach dem Aufbau auf jeden Fall ein Name gesetzt wurde und die Referenzvariable nicht standardmäßig null ist. Wenn wir es ermöglichen wollen, dass Entwickler neben den parametrisierten Konstruktoren auch einen parameterlosen Konstruktor nutzen können, müssten wir diesen per Hand hinzufügen.
Wie kann ein nützlicher Konstruktor aussehen?
Besitzt ein Objekt eine Reihe von Objektvariablen, so wird ein Konstruktor in der Regel diese Zustände initialisieren wollen. Wenn wir eine Unmenge von Objektvariablen in einer Klasse haben, sollten wir dann auch endlos viele Konstruktoren schreiben? Besitzt eine Klasse Objektvariablen, die durch set*(…)-Methoden gesetzt und durch get*()-Methoden gelesen werden, so ist es nicht unbedingt nötig, diese Objektvariablen im Konstruktor zu setzen. Ein parameterloser Konstruktor, der das Objekt in einen Initialzustand versetzt, ist angebracht; anschließend können die Zustände mit den Zugriffsmethoden verändert werden. Das sagt auch die JavaBean-Konvention. Praktisch sind sicherlich auch Konstruktoren, die die häufigsten Initialisierungsszenarien abdecken. Das Punkt-Objekt der Klasse java.awt.Point lässt sich mit dem parameterlosen Konstruktor erzeugen, aber auch mit einem parametrisierten, der gleich die Koordinatenwerte entgegennimmt; so sind vor dem ersten Zugriff alle Werte gegeben.
6.5.5 Copy-Konstruktor
Ein Konstruktor ist außerordentlich praktisch, wenn er ein typgleiches Objekt über seinen Parameter entgegennimmt und aus diesem Objekt die Startwerte für seinen eigenen Zustand nimmt. Ein solcher Konstruktor heißt Copy-Konstruktor.
Dazu ein Beispiel: Die Klasse Candy bekommt einen Konstruktor, der eine andere Süßigkeit entgegennimmt. Auf diese Weise lässt sich ein schon initialisiertes Candy-Objekt als Vorlage für eine neue Süßigkeit dem Konstruktor übergeben. Der parametrisierte Konstruktor kann alle (oder gezielt ausgewählte) Objektvariablen einer existierenden Süßigkeit auslesen und auf die neu entstehende Süßigkeit übertragen. Die Implementierung kann so aussehen:
public class Candy {
public String name;
public int price;
public Candy() {
}
public Candy( Candy other ) {
this.name = other.name;
this.price = other.price;
}
}
Testen wir das:
Candy sugarDaddy = new Candy();
sugarDaddy.name = "Sugar Daddy Caramel Pops";
sugarDaddy.price = 20;
Candy caramelPops = new Candy( sugarDaddy );
System.out.printf( "%s %d%n", // Sugar Daddy Caramel Pops 20
caramelPops.name, caramelPops.price );
Die main(…)-Methode erzeugt eine neue Süßigkeit sugarDaddy mit dem parameterlosen Konstruktor und initialisiert anschließend eine neue Süßigkeit caramelPops mit den Werten von sugarDaddy; wir nennen die Vorlage auch einen Prototyp.
[»] Hinweis
Wenn die Klasse Candy neben dem parametrisierten Konstruktor Candy(Candy) einen zweiten Konstruktor, Candy(Object), deklarieren würde, käme es bei einer Verwendung durch new Candy(sugarDaddy) auf den ersten Blick zu einem Konflikt, denn beide Konstruktoren würden passen. Der Java-Compiler löst das so, dass er immer den spezifischsten Konstruktor aufruft, also Candy(Candy) und nicht Candy(Object). Das gilt auch für new Candy(null) – auch hier wird der Konstruktor Candy(Candy) bemüht. Während diese Frage für den Alltag nicht so bedeutend ist, müssen sich Kandidaten der Zertifizierung zum Oracle Certified Professional Java Programmer auf eine solche Frage einstellen. Im Übrigen gilt bei den Methoden das gleiche Prinzip.
6.5.6 Einen anderen Konstruktor der gleichen Klasse mit this(…) aufrufen
Mitunter werden zwar verschiedene Konstruktoren angeboten, aber nur in einem Konstruktor verbirgt sich die tatsächliche Initialisierung des Objekts. Ändern wir ein wenig die City-Klasse, sodass sich einmal ein Objekt über einen Konstruktor City(String name, int population) initialisieren lässt und einmal mit einem Copy-Konstruktor City(City other):
public class City {
public String name;
public int population;
public City( String name, int population ) {
this.name = name.trim();
this.population = Math.max( 0, population );
}
public City( City other ) {
this.name = other.name.trim();
this.population = Math.max( 0, other.population );
}
}
Zu erkennen ist, dass beide Konstruktoren drei Aufgaben übernehmen: den Weißraum des Namens entfernen, darauf achten, dass die Population nicht negativ ist, und die Objektvariablen name und population initialisieren. Letztendlich fällt aber auf, dass der Code fast identisch ist. Dreimal kommen vor: Belegung der Objektvariablen, Weißraumentfernung, Bereichsprüfung.
Schlauer ist es, wenn der Konstruktor City(City) den Konstruktor City(String, int) der eigenen Klasse aufruft. Dann muss nicht gleicher Programmcode mehrfach vorkommen. Java lässt eine solche Konstruktorweiterleitung mit dem Schlüsselwort this zu:
public class City {
public String name;
public int population;
public City( String name, int population ) {
this.name = name.trim();
this.population = Math.max( 0, population );
}
public City( City other ) {
this( other.name, other.population );
}
public City() {
this( "Undefined", 0 );
}
}
Der Vorteil gegenüber der vorherigen Lösung ist, dass es nur eine zentrale Stelle gibt, die im Fall von Änderungen angefasst werden müsste. An trim() lässt sich der Vorteil schon ablesen: Vorher war das Trimmen in jedem Konstruktor eingepflegt, nach der Änderung nur lokal an einer Stelle. Nehmen wir an, wir hätten zehn Konstruktoren für alle erdenklichen Fälle in genau diesem Stil implementiert. Tritt der Fall ein, dass wir auf einmal in jedem Konstruktor etwas initialisieren müssen, so muss der Programmcode – etwa ein Aufruf der Methode init(…) – in jeden der Konstruktoren eingefügt werden. Dieses Problem umgehen wir einfach, indem wir die Arbeit auf einen speziellen Konstruktor verschieben. Ändert sich nun das Programm in der Weise, dass beim Initialisieren überall zusätzlicher Programmcode ausgeführt werden muss, dann ändern wir eine Zeile in dem konkreten, von allen benutzten Konstruktor. Damit fällt für uns wenig Änderungsarbeit an – unter softwaretechnischen Gesichtspunkten ein großer Vorteil. Überall in den Java-Bibliotheken lässt sich diese Technik wiedererkennen.
[»] Hinweis
Das Schlüsselwort this ist in Java mit zwei Funktionen belegt: Zum einen zeigt es als Referenz auf das aktuelle Objekt, und zum anderen formt es einen Aufruf zu einem anderen Konstruktor der gleichen Klasse, wenn es mit Klammern genutzt wird. Den Klassennamen als Methodenaufruf zu verwenden, also statt this(other.name, other.population) etwa City this(other.name, other.population) zu schreiben, funktioniert syntaktisch nicht, denn es könnte tatsächlich eine großgeschriebene Methode City(…) geben, die jedoch mit dem Konstruktor nichts zu tun hat.
Einschränkungen von this(…) *
Beim Aufruf eines anderen Konstruktors mittels this(…) gibt es zwei wichtige Beschränkungen:
-
Der Aufruf von this(…) muss die erste Anweisung des Konstruktors sein.
-
Vor dem Aufruf von this(…) im Konstruktor können keine Objekteigenschaften angesprochen werden. Das heißt, es darf weder eine Objektvariable als Argument an this(…) übergeben werden, noch darf eine andere Objektmethode der Klasse aufgerufen werden, die etwa das Argument berechnen möchte. Erlaubt ist nur der Zugriff auf statische Variablen (etwa finale Variablen, die Konstanten sind) oder aber der Aufruf statischer Methoden.
Die erste Einschränkung besagt, dass das Erzeugen eines Objekts immer das Erste ist, was ein Konstruktor leisten muss. Nichts darf vor der Initialisierung ausgeführt werden. Die zweite Einschränkung hat damit zu tun, dass die Objektvariablen erst nach dem Aufruf von this(…) initialisiert werden, sodass ein Zugriff unsinnig wäre – die Werte wären im Allgemeinen null:
public class Stereo {
static final int STANDARD = 1000;
/*non-static*/ final int standard = 1000;
public int watt;
public Stereo() {
// this( standard ); // Führt auskommentiert zum Compilerfehler:
// ^ Cannot refer to an instance field standard while
// explicitly invoking a constructor
this( STANDARD );
}
public Stereo( int watt ) {
this.watt = watt;
}
}
Da Objektvariablen bis zu einem bestimmten Punkt noch nicht initialisiert sind (was der nächste Abschnitt erklärt), lässt uns der Compiler nicht auf sie zugreifen – nur statische Variablen sind als Übergabeparameter erlaubt. Daher ist der Aufruf this(standard) nicht gültig, da standard eine Objektvariable ist; this(STANDARD) ist jedoch in Ordnung, weil STANDARD eine statische Variable ist.
6.5.7 Immutable-Objekte und Wither-Methoden
Objekte, deren Zustände nicht verändert werden können, heißen immutable. Die Klassen deklarieren in so einem Fall keine öffentlichen Variablen und auch keine Methoden mit Seiteneffekten, die diese Zustände modifizieren könnten. Setter gibt es folglich nicht, nur vielleicht Getter.
Damit die Objekte ihre Werte bekommen, gibt es unterschiedliche Wege – parametrisierte Konstruktoren sind ein guter Weg. Die Belegungen lassen sich beim Konstruktoraufruf übergeben und so sehr gut direkt in finale Variablen schreiben. In der Java-Bibliothek gibt es eine Reihe solcher Klassen, die keinen parameterlosen Konstruktor besitzen und nur einige parametrisierte, die Werte erwarten. Die im Konstruktor übergebenen Werte initialisieren das Objekt, und es behält diese Werte sein ganzes Leben lang. Zu den Klassen gehören zum Beispiel Integer, Double, Color, File oder Font.
Immutable-Objekte, die auch die equals(…)-Methode implementieren, heißen Werte-Objekt (engl. value object).
Finale Werte aus dem Konstruktor belegen
Eine finale Variable darf nur einmal belegt werden. Das bedeutet nicht zwingend, dass sie am Deklarationsort mit einem Wert belegt werden muss – das kann auch später passieren. Der Konstruktor darf zum Beispiel finale Objektvariablen beschreiben. Das Paar aus finaler Variable und initialisierendem Konstruktor ist ein häufig genutztes Idiom, wenn Variablenwerte später nicht mehr geändert werden sollen. So ist im Folgenden die Variable name final, da sie nur einmalig über den Konstruktor gesetzt und dann nur noch gelesen wird:
public class City {
public final String name;
public City( String name ) { this.name = name; }
public String getName() { return name; }
}
[»] Java-Stil
Immer dann, wenn sich bis auf die direkte Initialisierung vor Ort oder im Konstruktor die Belegung nicht mehr ändert, sollten Entwickler finale Variablen verwenden.
Wither-Methoden
Auch wenn sich Objekte mit Settern nicht ändern lassen, so soll es doch möglich sein, neue Objekte mit veränderten Zuständen zu erschaffen. Ein Blick auf die Klasse String zeigt zum Beispiel trim() und toUpperCase() – das Ergebnis sind neue Strings.
Um allgemein Zustandsvariablen zu verändern, können Wither-Methoden verwendet werden; sie sind ähnlich wie Setter, nur verändern sie keinen Zustand am aktuellen Objekt, sondern führen zu einem neuen Objekt mit dem geänderten Zustand.
Getter |
Setter |
Wither |
---|---|---|
Typ getXXX() |
void setXXX(Typ xxx) |
ImmutableTyp withXXX(Typ xxx) |
Dazu ein Beispiel: Eine Stadt hat einen Namen und eine Einwohneranzahl. Die Exemplare sollen unveränderbar (immutable) sein:
public class City {
public final String name;
public final int population;
public City( String name, int population ) {
this.name = name;
this.population = population;
}
public City withName( String name ) {
return new City( name, population );
}
public City withPopulation( int population ) {
return new City( name, population );
}
}
Die Objektvariablen können public sein, da ihre Werte von außen nicht veränderbar sind – so sparen wir unnötige Getter ein.
City almostKandy = new City( "H", 100 );
City kandy = almostKandy.withPopulation( 10_200 ).withName( "Hershey" );
System.out.printf( "%s %d", kandy.name, kandy.population ); // Hershey 10200
6.5.8 Ihr fehlt uns nicht – der Garbage-Collector
Glücklicherweise werden wir beim Programmieren in Java von der lästigen Aufgabe befreit, Speicher von Objekten freizugeben. Wird ein Objekt nicht mehr referenziert, findet der Garbage-Collector[ 154 ](Eine lange Tradition hat die automatische Speicherbereinigung unter LISP und unter Smalltalk, aber auch Visual Basic benutzt einen GC, und selbst das C64-BASIC nutzte eine Garbage-Collection für nicht mehr benötigte Zeichenketten. ) dieses Objekt und kümmert sich um alles Weitere – der Entwicklungsprozess wird dadurch natürlich vereinfacht. Der Einsatz der automatischen Speicherbereinigung verhindert zwei große Probleme:
-
Ein Objekt kann gelöscht werden, aber die Referenz existiert noch (engl. dangling pointer).
-
Kein Zeiger verweist auf ein bestimmtes Objekt, dieses existiert aber noch im Speicher (engl. memory leak).
[»] Hinweis
Konstruktoren sind besondere Anweisungsblöcke, die die Laufzeitumgebung immer im Zuge der Objekterzeugung aufruft. Sprachen wie C++, PHP, Python und Swift kennen auch das Konzept eines Destruktors, also eines besonderen Anweisungsblocks, der immer dann aufgerufen wird, wenn das Objekt nicht mehr benötigt wird.
Allgemeine Destruktoren kennt Java nicht. Für Ressourcen gibt es ein spezielles Sprachkonstrukt, sodass Ressourcen automatisch geschlossen werden können (siehe dazu Abschnitt 9.6, »try mit Ressourcen (automatisches Ressourcen-Management)«).
Die prinzipielle Arbeitsweise des Müllaufsammlers
Die automatische Speicherbereinigung erscheint hier als ominöses Ding, das die Objekte clever verwaltet. Doch wie arbeitet ein Garbage-Collector? Implementiert wird er als unabhängiger Thread mit niedriger Priorität. Er verwaltet die Wurzelobjekte, von denen aus das gesamte Geflecht der lebendigen Objekte (der sogenannte Objektgraph) erreicht werden kann. Dazu gehören die Wurzel des Thread-Gruppen-Baums und die lokalen Variablen aller aktiven Methodenaufrufe (Stack aller Threads). In regelmäßigen Abständen markiert der GC nicht benötigte Objekte und entfernt sie.
Dank der HotSpot-Technologie geschieht das Anlegen von Objekten unter der Java-VM von Oracle sehr schnell. HotSpot verwendet einen generationenorientierten GC, der den Umstand ausnutzt, dass zwei Gruppen von Objekten mit deutlich unterschiedlicher Lebensdauer existieren. Die meisten Objekte sterben sehr jung, die wenigen überlebenden Objekte werden hingegen sehr alt. Die Strategie dabei ist, dass Objekte im »Kindergarten« erzeugt werden, der sehr oft nach toten Objekten durchsucht wird und in der Größe beschränkt ist. Überlebende Objekte kommen nach einiger Zeit aus dem Kindergarten in eine andere Generation, die nur selten vom GC durchsucht wird. Damit folgt der GC der Philosophie von Joseph von Auffenberg, der meinte: »Verbesserungen müssen zeitig glücken; im Sturm kann man nicht mehr die Segel flicken.« Das heißt, die automatische Speicherbereinigung arbeitet ununterbrochen und räumt auf. Sie beginnt nicht erst mit der Arbeit, wenn es zu spät und der Speicher schon voll ist.
Die manuelle Nullung und Speicherlecks
Im folgenden Szenario wird die automatische Speicherbereinigung das nicht mehr benötigte Objekt hinter der Referenzvariablen reference entfernen, wenn die Laufzeitumgebung den inneren Block verlässt:
{
{
Candy reference = new Candy();
}
// Objekt hinter candy ist frei für den GC
}
In fremden Programmen sind mitunter Anweisungen wie die folgende zu lesen:
reference = null;
Oftmals sind sie unnötig, denn wie im Fall unseres Blocks weiß der GC, wann der letzte Verweis vom Objekt genommen wurde. Anders sieht das aus, wenn die Lebensdauer der Variablen größer ist, etwa bei einer Objekt- oder sogar bei einer statischen Variablen, oder wenn sie in einem Array referenziert wird. Wenn dann das referenzierte Objekt nicht mehr benötigt wird, sollte die Variable (oder der Array-Eintrag) mit null belegt werden, da andernfalls die automatische Speicherbereinigung das Objekt aufgrund der starken Referenzierung nicht wegräumen würde. Zwar findet die automatische Speicherbereinigung jedes nicht mehr referenzierte Objekt, aber die Fähigkeit zur Wahrsagerei, um Speicherlecks durch unbenutzte, aber referenzierte Objekte aufzuspüren, hat er nicht.