6.2 Heap-Größe und Freigabe von Speicher (Garbage Collection)

Die Java Objekte werden innerhalb des Javaprozesses in einem Speicherbereich mit dem Namen "Java Heap" verwaltet. In diesem Speicherbereich werden alle Datenstrukturen mit einer nicht festen Größe verwaltet. Diese Datenstrukturen sind:

  • Objekte (Instanzen von Klassen)
  • Felder von Basistypen oder Objekten
  • Zeichenketten (Sonderfall da Zeichenketten nicht mit dem new Operator angelegt werden müssen)

Dieser Bereich

  • hat eine initiale Größe
  • kann bis zu einem gegeben Limit wachsen
  • zwingt die Java Laufzeitumgebung eine OutOfMemory Ausnahme zu werfen wenn der Speicherbereich voll gelaufen ist.

Java VM Optionen

Optionen zum Heapmanagement(siehe auch Java Optionen (Referenzdokumentation)

  • initiale Heapgröße -Xms
  • maximale Heapgröße -Xmx
  • Protokollieren von Garbagekollektorläufen:-Xlog:gc 

Beispiel: Starten einer Anwendung Main.class mit 500 Megabytes initialem Heap (Freispeicher), 800 Megabytes maximalem Heap und Protokollierung von Garbagekollektorläufen:

java -Xms500m -Xmx800m -Xlog:gc Main

Nicht mehr referenzierte Objekte werden von der JavaVM automatisch, im Hintergrund von einem "Garbage Collector" gelöscht.

Dieser "Garbage Collector" (GC) kann manuell getriggert werden. Dies sollte man aber nicht in einem produktiven Programm durchführen. Der explizit angestossene GC bringt die Anwendung während seiner Arbeit zum Stehen! Automatisch ausgeführte GCs bewirken dies (normalerweise) nicht. Das explizite Anstoßen geschieht mit der statischem Methode System.gc(). Man kann sie wie folgt aufrufen:

System.gc();

Die Konsolenausgabe eines Programms welches mit der Option -Xlog:gc sieht wie folgt aus:

[GC 38835K->38906K(63936K), 0.1601889 secs]
[GC 39175K(63936K), 0.0050223 secs]
[GC 52090K->52122K(65856K), 0.1452102 secs]
[GC 65306K->65266K(79040K), 0.1433074 secs]

Sie zeigt an wieviel Speicher die Objekte von der "Garbage Collection" vorher und nachher belegen. Die benötigte Zeit wird ebenfalls angezeigt.

Verwalten von Objekten im Freispeicher (Heap)

Der Freispeicher (Heap) kann je nach Konfiguration eine konstante Größe haben oder er kann dynamisch bis zu einer vorgegebenen maximalen Größe wachsen. Der Entwickler ist daran interessiert, dass seine Anwendung alle benötigten Objekte im Freispeicher (Heap) verwalten kann. Kann das Javalaufzeitsystem keine neuen Objekte mehr anlegen wird es die Anwendung mit einer OutOfMemoryError Ausnahme beenden.

Dies wird normalerweise durch den Garbage-Collector der automatisch alle Objekte löscht die nicht mehr referenziert werden vermieden.

Nicht referenzierte Objekte

Nicht referenzierte Objekte können weder von der Anwendung noch vom Javalaufzeitsystem mit Hilfe von Referenzen erreicht werden. Dies bedeutet es gibt keine Kette von Referenzen zu einem Objekt die an den folgenden Orten beginnt:

  • von einer lokalen Referenzvariable im Systemstack
  • keine static Referenzvariable

Nicht referenzierte Objekte werden vom Javalaufzeitsystem bei Bedarf zu einem beliebigen Zeitpunkt gelöscht. Der genaue Zeitpunkt des Löschens ist nicht für den Entwickler vorhersehbar.

 

Beispiel

Im folgenden Programm werden eine Reihe von Personen erzeugt, die über eine Vater und Mutterbeziehung aufeinander referenzieren können.

 
package s1.block6.dereferenzieren;
 
public class Person {
 
    public Person vater;
    public Person mutter;
 
    public static void main (String[] args ) {
        Person p1 = new Person();
        Person p2 = new Person();
// Zeitpunkt 1
        p1.vater = p2;
        p2 = null;
// Zeitpunkt 2
        aufruf(p1);
        //Zeitpunkt 5
    }
 
    public static void aufruf(Person p) {
        Person[] persFeld = new Person[2];
// Zeitpunkt 3
        persFeld[1] = p;
        persFeld[0] = new Person();
        persFeld[0].vater = new Person();
        persFeld[0].mutter = new Person();
        // Zeitpunkt 4
    }
 
}

Das Hauptprogramm main() erzeugt zwei Instanzen der Klasse Person und ruft dann die Methode aufruf() auf die ein kleines Feld und einige weitere Instanzen erzeugt. 

Hinweis: Im folgenden Beispiel wird ein Feld verwendet. Javafelder werden im Detail im folgenden Kapitel erklärt.

Zum Zeitpunkt 1 sind in der main() Methode die folgenden Objekte erzeugt:

Zum Zeitpunkt 2 wurde in der main() Methode bereits die Vater-Referenz und der ursprüngliche Zeiger p2 mit null dereferenziert.

Zum Zeitpunkt 3 wurde die Methode aufruf() aufgerufen und das Feld persFeld mit zwei Feldern auf dem Heap angelegt.

Zum Zeitpunkt 4 sind drei weitere Personen angelegt worden. Die drei neuen Personen sind über den Index 0 von der Variablen persFeld erreichbar.
Zu diesem Zeitpunkt können alle Objekte auf dem Heap direkt, oder indirekt von Datenstrukturen auf dem Stapel (stack) erreicht werden.

Zum Zeitpunkt 5 wurde die Methode aufruf() bereits verlassen. Die lokalen Variablen der Methode aufruf() wurden vom Stack gelöscht und stehen nicht mehr zur Verfügung.

Hierdurch können eine Reihe von Instanzen der Klasse Person und das Feld auf dem Heap (Freispeicher) nicht mehr erreicht werden:

Die nicht mehr erreichbaren Objekte sind Müll (Garbarge) geworden und belegen verfügbaren Speicherplatz. Sie werden bei Bedarf vom Garbage-Collector (GC) gelöscht. Der Garbage-Collector wird alle Objekte löschen die vom Stack und statischen Variablen nicht mehr erreichbar sind. Dies hat für die Anwendung keine Implikationen da die Objekte auch von der Anwendung nicht mehr erreichbar sind. 

"Memory Leak"(engl. wikipedia)

Objekte die versehentlich bzw. ungewollt referenziert werden können nicht gelöscht werden. Diese Objekte können nach und nach den Heap füllen und zu einem Programmabbruch mangels Hauptspeicher (OutOfMemoryError Ausnahme) führen. Diesen Zustand der früher oder später zum ungewollten Abbruch eines Programms führen kann, nennt man im englischen "Memory Leak" (Speicherleck). Da man über die Zeit nutzbaren Speicher verliert wie ein Tank Wasser durch ein Leck verlieren kann.

Modifiziertes Beispiel (Memory Leak)

Ein Speicherleck kann durch eine minimale Änderung im vorhergehenden Beispiel entstehen. Gibt die Methode aufruf() als Ergebnis einen Zeiger auf das Feld von Personen zurück werden die drei Personen und das Feld nicht dereferenziert.

 
package s1.block6.dereferenzieren;
 
public class Person {
 
    public Person vater;
    public Person mutter;
 
    public static void main (String[] args ) {
        Person p1 = new Person();
        Person p2 = new Person();
        p1.vater = p2;
        p2 = null;
        Person[] f = aufruf(p1);
        //Zeitpunkt 7
    }
 
    public static Person[] aufruf(Person p) {
        Person[] persFeld = new Person[2];
        persFeld[1] = p;
        persFeld[0] = new Person();
        persFeld[0].vater = new Person();
        persFeld[0].mutter = new Person();
        // Zeitpunkt 6
return persFeld;
    }
}

 

Zum Zeitpunkt 6 sieht Objektmodell noch aus wie im vorhergehenden Beispiel:

 

 

Durch die Rückgabe der Referenz auf das Feld beim Beenden der Methode aufruf(), ist das Feld und die drei Objekte noch vom Stack erreichbar:

Die Variable f in main() referenziert das Feld. Das Feld wiederum referenziert 3 weitere Objekte.

Hier liegt ein Speicherleck nur vor, wenn der Entwickler nicht davon ausgeht, dass das Feld und alle referenzierten Objekte noch erreichbar sind. Ein Speicherleck ist kein Problem des Laufzeitsystems, da das Laufzeitsystem nicht zwischen noch benötigten und nicht mehr benötigten Objekten unterscheiden kann.

Dieses Problem wird vom Javaentwickler durch das Dereferenzieren von Objekten vermieden.

Dereferenzieren von Objekten

Der Javaentwickler muss nicht (und kann nicht) wie in anderen Programmiersprachen nicht mehr benötigte Objekte selbst löschen. Es verbleibt jedoch die Aufgabe sicherzustellen, dass nicht mehr benötigte Objekte nicht mehr referenziert werden um ein vollaufen des Heap zu vermeiden.

Implizites Dereferenzieren

Zeigt eine lokale Variable auf ein Objekt, so verschwindet die Referenz auf das Objekt mit dem Verlassen des Blocks in dem die lokale Referenzvariable definiert war.

Im der unten aufgeführten Methode warePrüfen() gibt es zwei Referenzvariablen w und w1 die auf eine Instanz einer Ware zeigen

public void warePrüfen(Ware w) {
   Ware w1 = w;
   ....
}

Beim Aufruf dieser Methode erhöht sich die Anzahl der Referenzen auf eine bestimmte Instanz der Klasse Ware um zwei Referenzen. Da die beiden Variablen aber am Ende des Blocks beim Verlassen der Methode wieder gelöscht werden erniedrigt sich die Anzahl der Referenzen auf ein gegebenes Objekt um zwei.

"Memory Leaks" entstehen daher typischerweise nicht durch lokale Variablen. Werden Methoden jedoch sehr spät verlassen (main() Methode!) werden auch die entsprechenden lokalen Referenzvariablen erst sehr spät gelöscht.

Beispiel

Die erste Variante des Beispiels in dem das Feld auf Personen nur innerhalb des Methodenblocks verwendet wurde ist ein Fall von impliziten Dereferenzieren

Explizites Dereferenzieren

Entwickler sollten Referenzvariablen auf Objekte explizit mit dem null belegen wenn Sie wissen, dass ein Objekt sicher nicht mehr benötigt wird.

o.ref = a-reference;
...
o.ref = null;

Das Derefenzieren einer Referenzvariable und den null Wert ist insbesondere wichtig wenn die Variable ein Attribut einer Klasse ist. Die Lebensdauer des Objekts o ist nicht unbedingt ersichtlich für den Entwickler. Das Setzen der Referenzvariablen o.ref auf null gewährleistet, dass das Objekt auf das mit der Variable a-reference gezeigt wird bei Bedarf gelöscht werden kann.

Beispiel

Das Speicherleck im zweiten Beispiel kann durch zwei verschiedene Änderungen vermieden werden:

1. Möglichkeit: Dereferenzieren  der Variable persFeld in der Methode aufruf()

public static Person[] aufruf(Person p) {
        Person[] persFeld = new Person[2];
        persFeld[1] = p;
        persFeld[0] = new Person();
        persFeld[0].vater = new Person();
        persFeld[0].mutter = new Person();
        // Zeitpunkt 1
        persFeld = null;
    return persFeld;
    }

2. Möglichkeit: Dereferenzieren der Variable f in der main() Methode:

public static void main (String[] args ) {
        Person p1 = new Person();
        Person p2 = new Person();
        p1.vater = p2;
        p2 = null;
        Person[] f = aufruf(p1);
        //Zeitpunkt 2
        f = null;
    }