CodeGym /Kurse /JAVA 25 SELF /Lokale Variablen, Speicherlecks, schwache Referenzen

Lokale Variablen, Speicherlecks, schwache Referenzen

JAVA 25 SELF
Level 64, Lektion 2
Verfügbar

1. Wo werden lokale Variablen gespeichert

Fangen wir mit dem Einfachsten an: lokale Variablen. Das sind Variablen, die innerhalb einer Methode deklariert werden und nur während ihrer Ausführung existieren. Ihr Leben ist kurz und dramatisch: Sobald die Methode beendet ist, verschwinden alle ihre lokalen Variablen spurlos.

In Java werden lokale Variablen auf dem Stack gespeichert. Jeder Thread hat seinen eigenen Stack. Wenn eine Methode eine andere Methode aufruft, wird dem Stack ein neuer „Frame“ (Stack Frame) mit lokalen Variablen und Rücksprungadresse hinzugefügt. Wenn die Methode ihre Arbeit beendet, wird ihr Frame vom Stack entfernt.

Beispiel: Das Leben einer lokalen Variable

public class LocalVariableDemo {
    public static void main(String[] args) {
        int a = 42; // die lokale Variable a lebt nur in main
        printSquare(a);
        // Hier gibt es die Variable b schon nicht mehr!
    }

    public static void printSquare(int b) {
        int square = b * b; // lokale Variable square
        System.out.println("Quadrat: " + square);
        // Nach dem Verlassen von printSquare verschwinden alle lokalen Variablen
    }
}

Wichtiger Punkt: Wenn die lokale Variable eine Referenz auf ein Objekt ist (z. B. String, Scanner, Array), dann lebt die Referenz selbst auf dem Stack, während das Objekt im Heap liegt! Wenn die Referenz verschwindet und es keine weiteren Referenzen auf das Objekt gibt, kann der Garbage Collector das Objekt entfernen.

Illustration

Stack (für main):
| int a = 42      |
| args            |
-------------------
Heap:
| [Objekte, die mittels new erstellt wurden] |

2. Speicherlecks in Java: Mythos oder Realität?

Viele Anfänger denken: „In Java gibt es doch einen Garbage Collector! Also kann es keine Speicherlecks geben!“ Leider ist das ein Mythos, der sich spätestens im ersten großen Projekt in Luft auflöst.

Der Garbage Collector entfernt nur jene Objekte, auf die es keine einzige lebendige Referenz mehr gibt. Wenn irgendwo eine Referenz übrig bleibt (und sei es am unerwartetsten Ort), bleibt das Objekt bis zum bitteren Ende im Speicher hängen. Oder bis zum OutOfMemoryError.

Beispiel 1: Statische Collection-Falle

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakDemo {
    // Ach, diese statische Sammlung!
    private static final List<String> BIG_LIST = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            BIG_LIST.add("Zeile Nummer " + i);
        }
        System.out.println("Eine Million Zeilen hinzugefügt");
        // Selbst wenn main endet, bleibt BIG_LIST im Speicher, solange die JVM lebt
    }
}

Was passiert?

  • Die statische Variable BIG_LIST lebt so lange, wie die Klasse aktiv ist (und das ist gewöhnlich bis zum Ende der JVM-Lebensdauer).
  • Alle dem List hinzugefügten Strings können vom Garbage Collector nicht entfernt werden – es gibt immer eine Referenz über BIG_LIST.
  • Wenn Sie vergessen, solche Collections zu leeren, bekommen Sie ein Speicherleck.

Beispiel 2: Nicht abgemeldete Listener

import java.util.ArrayList;
import java.util.List;

class EventSource {
    private final List<Runnable> listeners = new ArrayList<>();

    public void addListener(Runnable listener) {
        listeners.add(listener);
    }

    // ... weitere Methoden ...
}

public class ListenerLeakDemo {
    public static void main(String[] args) {
        EventSource source = new EventSource();
        Runnable listener = () -> System.out.println("Ereignis!");
        source.addListener(listener);
        // Wenn man vergisst, source.removeListener(listener) aufzurufen, bleibt listener für immer im Speicher!
    }
}

Problem: Wenn der Listener nicht mehr gebraucht wird, aber nicht aus der Liste entfernt wurde, bleiben er und alle Objekte, auf die er verweist, im Speicher.

Beispiel 3: Ein Cache, der niemals geleert wird

import java.util.HashMap;
import java.util.Map;

public class CacheLeakDemo {
    private static final Map<String, byte[]> CACHE = new HashMap<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            // Jedes Mal erzeugen wir ein Array mit 1 KB
            CACHE.put("key" + i, new byte[1024]);
        }
        System.out.println("Eine Million Elemente in den Cache eingefügt");
        // Der Cache wächst, der Speicher geht aus, OutOfMemoryError!
    }
}

Fazit: Selbst mit GC kann man leicht ein Speicherleck bekommen, wenn man den Lebenszyklus von Objekten nicht im Blick behält!

3. Schwache Referenzen (WeakReference) und ihre Freunde

Manchmal brauchen wir Caches oder Collections, in denen Objekte vom Garbage Collector entfernt werden dürfen, wenn niemand sonst mehr auf sie verweist. Dafür wurden schwache Referenzen (WeakReference) erfunden.

Normale (strong) Referenzen

String s = new String("hello"); // strong-Referenz

Das Objekt s bleibt im Speicher, solange es mindestens eine strong-Referenz gibt.

Schwache Referenz (WeakReference)

import java.lang.ref.WeakReference;

public class WeakRefDemo {
    public static void main(String[] args) {
        String strong = new String("Hallo, Welt!");
        WeakReference<String> weak = new WeakReference<>(strong);

        System.out.println("Vor der Bereinigung: " + weak.get()); // es gibt eine Referenz

        strong = null; // strong-Referenz entfernen

        System.gc(); // wir bitten den GC, Speicher zu bereinigen (nicht garantiert!)

        // Nach einiger Zeit kann weak.get() null werden
        System.out.println("Nach dem GC: " + weak.get());
    }
}

Wie funktioniert das?

  • Solange es mindestens eine strong-Referenz auf ein Objekt gibt, entfernt der GC es nicht.
  • Wenn nur noch schwache Referenzen übrig sind, kann das Objekt bei der nächsten Müllsammlung entfernt werden.
  • Die Methode weak.get() gibt das Objekt zurück, wenn es noch lebt, oder null, wenn es entfernt wurde.

Wo werden schwache Referenzen eingesetzt?

Die Hauptanwendung sind Caches, bei denen es nicht kritisch ist, wenn ein Objekt aus dem Speicher entfernt wird. Zum Beispiel, wenn Sie Bilder cachen, aber nicht möchten, dass der Cache den gesamten Speicher belegt.

Beispiel: WeakHashMap

WeakHashMap ist eine Collection, in der Schlüssel über schwache Referenzen gespeichert werden. Wenn niemand mehr auf den Schlüssel verweist, wird das entsprechende Paar aus der Map entfernt.

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        Map<Object, String> map = new WeakHashMap<>();
        Object key = new Object();
        map.put(key, "Wert");

        System.out.println("Vor der Bereinigung: " + map);

        key = null; // strong-Referenz auf den Schlüssel entfernen

        System.gc(); // wir bitten den GC zu arbeiten

        // Nach einiger Zeit wird die Map leer sein!
        try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        System.out.println("Nach dem GC: " + map);
    }
}

Achtung: WeakHashMap funktioniert nur für Schlüssel – die Werte werden mit normalen strong-Referenzen gespeichert.

Soft, Weak, Phantom: die ganze Referenzfamilie

In Java gibt es vier Referenztypen (sortiert nach „Stärke“):

Referenztyp Wann entfernt der GC das Objekt? Wo eingesetzt?
Strong
Nur wenn es keine einzige strong-Referenz mehr gibt Gewöhnliche Variablen, Collections
Soft
Bei Speichermangel Cache, der möglichst lange gehalten werden soll
Weak
Beim nächsten GC-Lauf, sofern es keine strong-Referenzen mehr gibt Caches, WeakHashMap, Listener
Phantom
Nach der Finalisierung, zum Verfolgen der Objektentfernung Spezielle Aufgaben, Bereinigung außerhalb des Heaps
  • SoftReference – das Objekt wird bei Speichermangel entfernt (geeignet z. B. für Bild-Caches).
  • WeakReference – wird bei der ersten Müllsammlung entfernt, wenn es keine anderen Referenzen mehr gibt.
  • PhantomReference – der „gespenstischste“ Typ, für komplexe Szenarien gedacht, wird von Einsteigern selten verwendet.

4. Praxis: Beispiel für ein Speicherleck und dessen Behebung

Leck-Beispiel: Statische Liste

import java.util.ArrayList;
import java.util.List;

public class LeakExample {
    private static final List<byte[]> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100_000; i++) {
            list.add(new byte[1024 * 1024]); // 1 MB
            if (i % 10 == 0) System.out.println("Hinzugefügt " + i + " MB");
        }
    }
}

Was wird passieren?
Das Programm frisst schnell den gesamten verfügbaren Speicher und stürzt mit OutOfMemoryError ab, weil die statische Liste Referenzen auf alle erstellten Arrays hält.

Behebung: schwache Referenzen verwenden

Wenn es nicht kritisch ist, dass alle Objekte jederzeit verfügbar sind, kann man sie über schwache Referenzen speichern:

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class LeakFixed {
    private static final List<WeakReference<byte[]>> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100_000; i++) {
            list.add(new WeakReference<>(new byte[1024 * 1024]));
            if (i % 10 == 0) System.out.println("Hinzugefügt " + i + " MB");
            System.gc(); // Hinweis für den GC (garantiert keine sofortige Bereinigung!)
        }
    }
}

Jetzt können Arrays vom GC entfernt werden, wenn niemand sonst mehr auf sie verweist – die Liste speichert nur schwache Referenzen.

5. Typische Szenarien für Speicherlecks

  • Ereignis-Listener: Listener nicht entfernt – Objekt lebt ewig.
  • Statische Collections: nicht geleerte Caches, globale Listen – all das kann zu Lecks führen.
  • Innere Klassen und Lambdas: wenn eine innere Klasse oder Lambda eine Referenz auf das äußere Objekt einfängt, wird es nicht entfernt, solange das äußere Objekt lebt.

Beispiel mit innerer Klasse

public class Outer {
    private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB

    public Runnable createTask() {
        // Eine anonyme innere Klasse fängt eine Referenz auf Outer ein!
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("Aufgabe wird ausgeführt");
            }
        };
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        Runnable task = outer.createTask();
        // Selbst wenn outer = null ist, hält task dennoch eine Referenz auf bigArray!
    }
}

Lösung: Verwenden Sie statische innere Klassen oder lagern Sie die Logik in separate Klassen aus, um keine überflüssigen Referenzen zu halten.

6. Praxis: Einsatz schwacher Referenzen im Cache

Fügen wir einer Übungsanwendung einen einfachen Cache mit WeakHashMap hinzu.

import java.util.Map;
import java.util.WeakHashMap;

public class ImageCache {
    private final Map<String, byte[]> cache = new WeakHashMap<>();

    public void put(String name, byte[] data) {
        cache.put(name, data);
    }

    public byte[] get(String name) {
        return cache.get(name);
    }

    public static void main(String[] args) {
        ImageCache cache = new ImageCache();
        cache.put("cat", new byte[1024 * 1024]); // 1 MB
        System.out.println("Katze zum Cache hinzugefügt");

        // Wenn es keine Referenzen mehr auf den Schlüssel "cat" gibt, kann das Objekt vom GC entfernt werden
    }
}

In realen Anwendungen (z. B. in Bildbibliotheken) helfen schwache Referenzen, Speicherauslastung zu vermeiden, indem selten verwendete Daten automatisch entfernt werden.

7. Strong vs Weak: wann was verwenden?

  • Strong-Referenzen – Standard: verwenden Sie sie für alles, was garantiert leben soll.
  • Schwache Referenzen – für Caches, Listener, wenn es nicht kritisch ist, dass ein Objekt entfernt wird.
  • Soft-Referenzen – für Caches, die möglichst lange gehalten werden sollen, aber bei Speichermangel entfernt werden dürfen.
  • Phantom-Referenzen – für fortgeschrittene Szenarien (z. B. Finalisierung außerhalb des Heaps).

8. Typische Fehler beim Umgang mit Speicher und Referenzen

Fehler Nr. 1: „Der GC räumt schon alles für mich auf!“ Der Garbage Collector entfernt nur jene Objekte, auf die es keine einzige lebendige strong-Referenz gibt. Wenn Sie irgendwo eine Referenz „vergessen“ (z. B. in einer static-Collection), lebt das Objekt ewig.

Fehler Nr. 2: Vergessene Listener. Listener zu einem Objekt hinzugefügt, aber beim Zerstören des Objekts nicht entfernt? Der Listener und alles, was er eingefangen hat, bleibt im Speicher.

Fehler Nr. 3: Cache ohne schwache Referenzen. Verwenden Sie eine normale HashMap für einen Cache, der sich automatisch leeren soll? Verwenden Sie besser WeakHashMap oder SoftReference.

Fehler Nr. 4: Innere Klassen und Lambdas fangen das äußere Objekt ein. Innere Klassen (und Lambda-Ausdrücke) halten implizit eine Referenz auf das äußere Objekt. Wenn Sie Instanzen solcher Klassen länger aufbewahren als das äußere Objekt selbst, bekommen Sie ein Leck.

Fehler Nr. 5: Erwartung einer sofortigen GC-Ausführung. Der Aufruf von System.gc() garantiert nicht, dass die Müllsammlung sofort stattfindet. Das ist nur eine „Bitte“ an die JVM und kein Befehl.

1
Aufgabe
JAVA 25 SELF, Level 64, Lektion 2
Gesperrt
Verwendung einer schwachen Referenz – Nachricht in verschwindender Tinte 👻
Verwendung einer schwachen Referenz – Nachricht in verschwindender Tinte 👻
1
Aufgabe
JAVA 25 SELF, Level 64, Lektion 2
Gesperrt
Cache mit WeakHashMap – Die smarte Bibliothek, die vergessene Bücher wiederverwertet 📚
Cache mit WeakHashMap – Die smarte Bibliothek, die vergessene Bücher wiederverwertet 📚
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION