CodeGym /Kurse /JAVA 25 SELF /Analyse typischer Fehler beim Umgang mit Speicher

Analyse typischer Fehler beim Umgang mit Speicher

JAVA 25 SELF
Level 64 , Lektion 4
Verfügbar

1. Typische Fehler im Umgang mit Speicher

Zeit, die Kehrseite der Magie der automatischen Speicherverwaltung zu betrachten. Selbst wenn Sie nicht in C programmieren, wo man jedem Byte persönlich nachspüren muss, kann man in Java so viel falsch machen, dass die Anwendung Speicher frisst wie ein verfressener Kater – Wurst. Schauen wir uns die häufigsten Fehler an und wie man sie vermeidet.

Vergessene Listener (listeners)

In Java ist das Muster „Listener“ weit verbreitet – ein Objekt, das sich auf Ereignisse eines anderen Objekts registriert. Beispiel: Sie haben eine Schaltfläche erstellt und ihr einen Klick-Handler hinzugefügt:

button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // Klick verarbeiten
    }
});

Problem: Wenn Sie diesen Listener (removeActionListener) nicht entfernen, wenn die Schaltfläche oder das Fenster nicht mehr benötigt werden, bleibt der Listener im Speicher hängen. Selbst wenn Sie das Fenster geschlossen und alle Referenzen darauf auf null gesetzt haben, hält das Listener-Objekt immer noch eine Referenz auf das Fenster (oder umgekehrt) und verhindert so, dass der Garbage Collector den Speicher freigibt.

Analogie: Stellen Sie sich vor, Sie sind umgezogen, haben aber vergessen, den Newsletter der Pizzeria abzubestellen – die Werbung kommt weiter an die alte Adresse.

Statische Collections, die nicht geleert werden

Statische Felder leben so lange wie die Klasse (und manchmal – bis zum Ende der Anwendung). Wenn Sie eine statische Collection haben:

public class Cache {
    public static final List<String> globalList = new ArrayList<>();
}

und Sie fügen dort Objekte hinzu, entfernen sie aber nicht, bleiben sie für immer im Speicher. Selbst wenn es sonst keine Referenzen auf die Objekte mehr gibt, verhindert die Referenz aus der statischen Collection, dass der GC sie entfernt.

Reales Beispiel: Ein Fotocache in einer Desktop-Anwendung, der nie bereinigt wird. Nach ein paar Stunden Laufzeit – OutOfMemoryError.

Ressourcen nicht freigeben (Dateien, Streams, Verbindungen)

Zwar gibt Java Speicher frei, kümmert sich aber nicht automatisch um das Schließen von Dateideskriptoren, Netzwerkverbindungen und anderen externen Ressourcen. Wenn man vergisst, eine Datei oder einen Stream zu schließen, bleibt die Ressource hängen, und irgendwann sagt das System: „Schluss, keine Dateien mehr!“ (IOException: Too many open files).

Tipp: Verwenden Sie immer try-with-resources:

try (FileInputStream in = new FileInputStream("data.txt")) {
    // Datei lesen
} // in.close() wird automatisch aufgerufen!

Große Objekte, die lange im Speicher hängen

Manchmal erstellen Sie ein großes Array oder eine Collection, verwenden es und „vergessen, es freizugeben“. Zum Beispiel:

List<byte[]> bigList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    bigList.add(new byte[1024 * 1024]); // je 1 MB
}
// ... bigList leeren vergessen

Wenn diese Collection in einem statischen Feld lebt oder in einem Objekt, das lange nicht gelöscht wird, bleibt all dieser Speicher belegt.

Innere und anonyme Klassen: Capture externer Referenzen

Anonyme (und innere) Klassen in Java speichern eine implizite Referenz auf das äußere Objekt:

public class Outer {
    void doSomething() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from inner!");
            }
        };
        // r wird irgendwo gespeichert
    }
}

Wenn das Objekt r in einer statischen Collection oder in einem Cache landet, „hält“ es eine Referenz auf die Instanz von Outer, selbst wenn diese nicht mehr benötigt wird. Ergebnis: Speicherleck. Bei Lambda-Ausdrücken ist die Situation etwas besser, aber wenn die Lambda Felder der äußeren Klasse verwendet, bleibt die Referenz dennoch erhalten.

2. Fehler im Umgang mit dem Garbage Collector

Erzwungener Aufruf von System.gc()

Viele Einsteiger denken: „Der Speicher wird knapp – ich rufe System.gc() auf und alles ist gelöst!“. In Wirklichkeit ist das nur eine Bitte an die JVM und keine Garantie für eine sofortige Sammlung. Häufige Nutzung kann die Leistung deutlich verschlechtern, lange Pausen und Freezes verursachen. In realen Anwendungen sollten Sie der JVM vertrauen – sie entscheidet selbst, wann Müll gesammelt wird. Übrigens können manche JVMs explizite GC-Aufrufe ignorieren (z. B. mit der Option -XX:+DisableExplicitGC).

GC-Logs ignorieren

In den GC-Logs sieht man, wann Sammlungen stattfinden, wie lange sie dauern und wie viel Speicher freigegeben wird. Wer diese Logs nicht betrachtet, kann Warnsignale übersehen: lange Pausen, häufige Full GCs, Speicherlecks.

So aktivieren Sie GC-Logs:

java -Xlog:gc* -jar MyApp.jar

oder für ältere JVMs:

java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar MyApp.jar

Falsche Wahl des GC für die Aufgabe

Die Wahl des Collectors beeinflusst Latenz und Stabilität. Für niedrige Latenz (Börsen, Online-Spiele) ist der parallele Stop-the-world-Parallel GC eine schlechte Idee: Er kann alle Threads für die Dauer der Sammlung „einfrieren“. Ziehen Sie G1 GC, ZGC oder Shenandoah in Betracht.

3. Fehler mit Collections

Einsatz von HashMap statt WeakHashMap für Caches.
Wenn Sie einen Cache bauen, in dem Objekte automatisch entfernt werden sollen, sobald es keine „lebenden“ Referenzen mehr auf sie gibt, verwenden Sie WeakHashMap:

Map<Key, Value> cache = new WeakHashMap<>();

Bei einer normalen HashMap leben Objekte so lange, bis der Cache manuell geleert wird – das führt zu Speicherlecks.

Vergessenes remove() für Elemente.
Wenn Sie Objekte zu Collections hinzufügen (z. B. zu Listener-Listen), sie aber nicht entfernen, wenn sie nicht mehr benötigt werden, bleiben diese Objekte ewig erhalten, insbesondere in langlebigen Collections (z. B. statischen).

4. Best Practices: wie man Probleme vermeidet

Listener immer entfernen.
Wenn sich ein Objekt auf Ereignisse registriert hat, melden Sie es unbedingt ab, wenn es nicht mehr benötigt wird. Praktisch ist es, dies in der Methode dispose() oder beim Schließen des Fensters/Bildschirms zu tun.

button.removeActionListener(myListener);

Schwache Referenzen für Caches verwenden.
Wenn ein Cache ohne Garantie auf den Erhalt eines Objekts auskommt, verwenden Sie WeakReference oder darauf basierende Collections (WeakHashMap). So kann der GC Speicher freigeben, wenn er benötigt wird.

Speicher im Produktivbetrieb überwachen.
Verwenden Sie jvisualvm, jconsole oder APM-Systeme. So lassen sich Lecks entdecken, bevor Nutzer sich beschweren.

Heap Dump bei Verdacht auf ein Leck analysieren

Wenn die Anwendung plötzlich mehr Speicher verbraucht als gewöhnlich, erstellen Sie einen Heap Dump (z. B. über jmap oder jvisualvm) und schauen Sie, welche Objekte am meisten Platz einnehmen. Oft ist der Schuldige in wenigen Minuten gefunden.

JVM-Parameter konfigurieren.

  • -Xmx – maximale Heap-Größe
  • -Xms – anfängliche Heap-Größe

Vernünftige Limits helfen, OutOfMemoryError zu vermeiden und die Diagnose zu beschleunigen.

5. Praxis: Beispielcode mit Speicherleck und dessen Behebung

Beispiel 1: Leck durch statische Collection

public class MemoryLeakDemo {
    // Statische Liste – lebt ewig
    private static final List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            leakyList.add(new byte[1024 * 1024]); // jedes Mal 1 MB
            System.out.println("Hinzugefügt " + (i + 1) + " MB");
        }
        // OutOfMemoryError!
    }
}

Behebung: Verwenden Sie eine lokale Variable oder leeren Sie die Collection, wenn sie nicht mehr benötigt wird.

public class MemoryLeakFixed {
    public static void main(String[] args) {
        List<byte[]> tempList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            tempList.add(new byte[1024 * 1024]);
            System.out.println("Hinzugefügt " + (i + 1) + " MB");
        }
        // tempList = null; // Kann explizit auf null gesetzt werden
        // Objekte sind nach dem Verlassen der Methode für den GC freigegeben
    }
}

Beispiel 2: Leck über Listener

public class Window {
    private final List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener l) {
        listeners.add(l);
    }
    // Keine removeListener-Methode!
}

Behebung: Fügen Sie eine Methode zum Entfernen des Listeners hinzu und rufen Sie sie beim Schließen des Fensters auf.

public void removeListener(EventListener l) {
    listeners.remove(l);
}

Beispiel 3: Cache mit HashMap statt WeakHashMap

Map<Object, Object> cache = new HashMap<>();
// ... Objekte hinzufügen

Behebung: Wechseln Sie zu WeakHashMap:

Map<Object, Object> cache = new WeakHashMap<>();

Tipps zur JVM-Konfiguration für Speicherüberwachung

  • GC-Logs einschalten: -Xlog:gc* oder -XX:+PrintGCDetails
  • Maximale Heap-Größe begrenzen: -Xmx512m
  • Wenn Sie Caches verwenden – überwachen Sie deren Größe und setzen Sie schwache Referenzen ein, wenn das vertretbar ist
  • Mit dem GC experimentieren: -XX:+UseG1GC, -XX:+UseZGC, -XX:+UseShenandoahGC

7. Typische Fehler im Umgang mit Speicher

Fehler Nr. 1: Vergessene Listener und Subscriptions. Wenn Sie einem Objekt einen Listener hinzugefügt haben, ihn aber nicht entfernen, bleibt das Listener-Objekt (und alles, worauf es verweist) im Speicher. Klassiker bei GUI- und Eventsystemen. Verwenden Sie removeListener/removeActionListener.

Fehler Nr. 2: Statische Collections ohne Bereinigung. Statische Felder leben am längsten. Wenn Sie dort Objekte ablegen und die Collection nicht bereinigen, bleiben diese Objekte für immer im Speicher. Besonders tückisch bei unbeschränkten Caches.

Fehler Nr. 3: Externe Ressourcen nicht freigeben. Einen Stream, eine Datei oder eine Verbindung offen gelassen? Sie verlieren Speicher und stoßen an OS-Limits. Verwenden Sie try-with-resources und schließen Sie Ihre Ressourcen.

Fehler Nr. 4: Erzwungener Aufruf von System.gc(). Das ist kein Allheilmittel, sondern nur eine Bitte an die JVM. Führt oft zu Pausen und Performance-Degradation.

Fehler Nr. 5: Normale Collections für Caches. Wenn sich Objekte im Cache selbst entfernen sollen, nutzen Sie schwache/Soft‑Referenzen (WeakHashMap, SoftReference). Andernfalls riskieren Sie ein Leck.

Fehler Nr. 6: Innere und anonyme Klassen, die äußere Referenzen capturen. Innere Klassen und Lambdas können implizit eine Referenz auf das äußere Objekt halten. Wenn man sie in einer langlebigen Collection speichert – entsteht ein Leck.

Fehler Nr. 7: GC-Logs ignorieren. Wenn Sie keine GC-Logs ansehen, erfahren Sie nichts über lange Pausen oder häufige Full GCs – die Nutzer merken es an Lags und Freezes. Aktivieren Sie -Xlog:gc* oder -XX:+PrintGCDetails.

1
Umfrage/Quiz
Speicher und Garbage Collection, Level 64, Lektion 4
Nicht verfügbar
Speicher und Garbage Collection
Speicher und Garbage Collection
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION