Lexikon · S

Speicherleck (Memory Leak)

Speicher, den ein Programm belegt und weiter festhält, obwohl er nicht mehr gebraucht wird, bis der Prozess langsam wird oder beendet wird.

Von Obseria-RedaktionAktualisiert

Definition

Ein Speicherleck (Memory Leak) ist Speicher, den ein Programm belegt, aber nie wieder freigibt, obwohl er nicht mehr benötigt wird. Der Verbrauch wächst mit Zeit oder Last, bis der Prozess durch Garbage-Collection-Druck langsam wird, auslagert oder vom Betriebssystem bzw. der Container-Runtime wegen Speichermangels beendet wird.

Was ist ein Speicherleck?

Ein Speicherleck (Memory Leak) entsteht, wenn ein Programm Speicher festhält, den es nie wieder nutzen wird. In Sprachen mit manueller Speicherverwaltung wie C und C++ ist der klassische Fall Speicher, der mit malloc oder new angefordert und nie freigegeben wird. In Runtimes mit Garbage Collector wie JVM, .NET, Node.js, Python oder Go werden nur unerreichbare Objekte freigegeben. Ein Leck bedeutet dort, dass noch irgendetwas eine Referenz auf fachlich längst tote Daten hält. Für den Collector sind diese Daten lebendig, also werden sie nie zurückgewonnen.

Speicherlecks führen selten zu einem sofortigen Ausfall. Sie zeigen sich als langsam steigender Speicherverbrauch, längere und häufigere Pausen der Garbage Collection und schließlich als Absturz oder OOM Kill, oft Stunden oder Tage nach dem Deployment, das sie verursacht hat.

Typische Ursachen für Speicherlecks

  • Unbegrenzte Caches und Maps. Eine statische Map als Cache ohne Größenlimit oder Verdrängung wächst mit jedem neuen Schlüssel.
  • Listener und Callbacks, die nie entfernt werden. Event-Handler, Observer oder Subscriptions halten ihre Besitzer am Leben.
  • Thread-lokale und request-bezogene Daten, die beim Wiederverwenden gepoolter Threads nicht gelöscht werden.
  • Closures in JavaScript, die große Objekte einfangen, oder Timer und Intervalle, die nie beendet werden.
  • Goroutine- oder Thread-Lecks: blockierte Worker, die nie enden und jeweils ihren Stack und referenzierte Daten behalten.
  • Nativer Speicher außerhalb des verwalteten Heaps, etwa Direct Buffers, native Bibliotheken oder nicht geschlossene Datei- und Socket-Handles.
  • Prozessinterne Metriken mit hoher Kardinalität, bei denen jeder neue Label-Wert eine weitere Zeitreihe im Speicher anlegt.

Ein Speicherleck erkennen

Das entscheidende Signal ist nicht hoher Speicherverbrauch, sondern Speicher, der nicht wieder sinkt. Ein gesunder Prozess mit Garbage Collector zeigt ein Sägezahnmuster: Der Verbrauch steigt mit Allokationen und fällt nach jeder Collection. Bei einem Leck klettern die Tiefpunkte des Sägezahns mit der Zeit nach oben. Hilfreiche Signale sind:

  • Heap-Belegung nach der Garbage Collection, etwa die OpenTelemetry-Metrik jvm.memory.used_after_last_gc für Java.
  • Resident Set Size des Prozesses und in Kubernetes das Working Set des Containers im Vergleich zu seinem Memory-Limit.
  • GC-Zeit und -Häufigkeit: Je weniger freier Platz bleibt, desto öfter läuft der Collector und desto weniger gewinnt er zurück.
  • Zusammenhang mit Deployments oder Last: Ein Leck pro Request skaliert mit dem Traffic, ein Leck pro Stunde deutet auf Hintergrundaufgaben.

Der Vergleich des aktuellen Verlaufs mit einer Baseline früherer Releases macht langsame Lecks sichtbar, lange bevor sie einen Ausfall verursachen.

Beispiel: Leck bestätigen und lokalisieren

Ein illustratives Szenario: Ein Java-Service wird alle paar Tage mit Exit-Code 137 neu gestartet. Das Dashboard zeigt, dass der Heap nach der GC innerhalb von 48 Stunden von etwa 300 MB auf 1,4 GB steigt, während der Traffic konstant bleibt. Last scheidet damit als Ursache aus, alles deutet auf ein Leck. Das Team geht so vor:

  1. Es erstellt im Abstand von einigen Stunden zwei Heap-Dumps (jcmd <pid> GC.heap_dump) und vergleicht die dominierenden Objekttypen.
  2. Es findet eine wachsende HashMap mit der Session-ID als Schlüssel, die beim Login befüllt, beim Logout aber nie bereinigt wird.
  3. Es ersetzt sie durch einen begrenzten Cache mit zeitbasiertem Ablauf und prüft, dass der Heap nach der GC nach dem Fix wieder flach verläuft.

In anderen Runtimes unterscheiden sich die Werkzeuge, etwa Heap-Snapshots in den Chrome DevTools für Node.js, tracemalloc für Python oder pprof-Heap-Profile für Go. Das Vorgehen bleibt gleich: Trend bestätigen, Snapshots vergleichen, den festhaltenden Referenzpfad finden.

Best Practices

  • Geben Sie jedem Cache eine Maximalgröße und eine Verdrängungsstrategie.
  • Stellen Sie jedem Subscribe ein Unsubscribe und jedem Öffnen ein Schließen gegenüber, idealerweise über Sprachmittel wie try-with-resources oder defer.
  • Führen Sie Soak-Tests durch: Lange Lasttests bei konstantem Traffic decken Lecks auf, die kurze Tests übersehen. Vergleichen Sie dabei Heap-Profile vom Anfang und vom Ende des Tests.
  • Alarmieren Sie auf den Trend des Speichers nach der GC oder auf die prognostizierte Zeit bis zum Limit, nicht nur auf einen statischen Schwellenwert.
  • Verlassen Sie sich nicht auf regelmäßige Neustarts als Lösung. Sie verdecken das Leck und machen daraus einen wiederkehrenden Incident.

Wie Obseria hilft

Obseria erfasst Laufzeitmetriken wie Heap, GC und Container-Speicher über OpenTelemetry und kann steigende Speichertrends per Anomalieerkennung markieren. Continuous Profiling zeichnet Allokationsprofile in Produktion auf. So vergleichen Sie einen Flame Graph vor und nach einem Release und sehen, welcher Codepfad Speicher anlegt und festhält. JVM-spezifische Details finden Sie unter Java-Monitoring.

Häufig gestellte Fragen

Ja. Ein Garbage Collector gibt nur unerreichbare Objekte frei. Referenziert ein Cache, eine Listener-Liste oder eine statische Collection noch Daten, die nicht mehr gebraucht werden, bleiben diese im Speicher und der Heap wächst weiter.

Betrachten Sie den Speicher nach der Garbage Collection über einen längeren Zeitraum bei stabiler Last. Steigt das Minimum immer weiter, statt sich auf einem Niveau einzupendeln, liegt wahrscheinlich ein Leck vor. Hoher, aber flacher Verbrauch ist meist eine Frage der Dimensionierung.

Das Speicherleck ist die Ursache: Speicher, der nie freigegeben wird. Der OOM Kill ist eine mögliche Folge: Kernel oder Container-Runtime beenden den Prozess, weil er den verfügbaren Speicher oder sein Limit überschritten hat.

Nur kurzfristig, solange die Ursache untersucht wird. Neustarts verdecken den Trend, kosten Requests und kalte Caches, und das Leck kehrt mit jedem Deployment zurück.

Setzen Sie das Wissen in die Praxis um

Testen Sie Obseria 14 Tage kostenlos, mit OpenTelemetry und ohne proprietäre Agenten.