© DrHitch/Shutterstock.com
Memory Leaks in Java

1 Garbage Collection und Memory Management


Da sich Oracle mit Java 8 noch ein wenig Zeit lässt (als anvisierter Termin wird im Moment „Frühjahr 2014“ genannt), wollen wir die Gelegenheit nutzen, um mit diesem shortcut noch einmal an die Artikelserie über Garbage Collection aus den Jahren 2010/11 anzuschließen. Wir hatten damals die Garbage Collection in der HotSpot-JVM von Sun/Oracle sowie ihr Tuning detailliert besprochen. Bei Interesse kann man diese Artikel weiterhin auf unserer Webseite [1] oder zusammengefasst als Buch [2] finden. Wir wollen anknüpfend an dieses zurückliegende Thema diskutieren, wie es zu Memory Leaks in Java kommen kann und wie man sie finden oder besser schon von vornherein vermeiden kann.

Bei der Erwähnung von Memory Leaks mag man sich Leser fragen, was denn das sein soll – ein Memory Leak in Java. Gibt es so etwas überhaupt? Eigentlich wurde doch beim Wechsel hin zu Java und weg von C bzw. C++ (also Programmiersprachen mit explizitem Memory Management durch den Programmierer) versprochen, dass Probleme wie Memory Leaks wegen dem automatischen Memory Management mittels Garbage Collector ein für alle Mal passé seien. Es stimmt schon: Memory Leaks wie in C/C++, die auf Grund von vergessenem free() bzw. delete entstehen, gibt es in Java glücklicherweise nicht mehr. Das hat den Vorteil, dass Memory Leaks in Java deutlich seltener auftreten als in C/C++. Ganz offensichtlich treten Memory Leaks in Java so selten auf, dass es eine Herausforderung ist, eines zu erzeugen. Es soll vorgekommen sein, dass Java-Entwickler in Bewerbungsgesprächen gebeten wurden, Memory Leaks zu erzeugen [3], um ihre Fähigkeiten unter Beweis zu stellen. Der Nachteil ist, dass Memory Leaks in Java meist keine Flüchtigkeitsfehler bei der Programmierung sind, sondern häufig eher Konzept- bzw. Designfehler.

Was macht der Garbage Collector?

Der Ausgangspunkt für Memory Leaks in Java ist die automatische Garbage Collection, denn allein der Garbage Collector entscheidet, wann Objekte bzw. ihr Speicher freigegeben werden. Wie macht er das? Bei allen Sun/Oracle Garbage Collectoren (außer dem Garbage First („G1“) Collector, [4]) wird dazu ein Marking angewandt. Vereinfacht beschrieben sieht das so aus, dass ausgehend von den so genannten Root References alle erreichbaren Objekte markiert werden. Danach weiß der Garbage Collector, dass die markierten Objekte erreichbar sind und geht davon aus, dass diese weiter im Programm genutzt werden. Umgekehrt weiß er auch, dass er alle nicht markierten Objekte freigegeben kann. Im Detail ist das Marking, je nach Garbage-Collector-Algorithmus, eine ziemlich komplizierte Angelegenheit, die aus mehreren Phasen bestehen kann. Wer sich dafür interessiert findet die Details unter [5], [6] und [7]. Die Root References sind, wie der Name schon andeutet, die Ausgangsreferenzen zu allen Objekten in einer JVM. Beispiele sind die Stack Pointer zu allen Thread Stacks. Variablen von Referenztypen, die als Methodenparameter oder methodenlokale Variablen auf dem Stack angelegt werden, sind über diese Root References erreichbar und folglich markiert.

Wie passt die Garbage-Collection-Strategie mit dem Programm zusammen?

Fassen wir das eben Gesagte noch mal kurz zusammen: Mit dem Marking bestimmt der Garbage Collector, welche Objekte erreichbar sind und noch verwendet werden (können). Diese Sicht des Garbage Collectors („erreichbar“ = „verwendet“) stimmt mit der Sicht unseres Programms im Allgemeinen überein; es kann aber auch zu Abweichungen kommen. So kann es Objekte geben, die nicht mehr im weiteren Ablauf vom Code unseres Programms verwendet werden, die aber trotzdem weiterhin über eine Verweiskette von Root Referenences aus erreichbar sind. Diese Objekte sind sozusagen in der Programmlogik in Vergessenheit geraten, der Garbage Collector erkennt sie aber weiterhin als „erreichbar“. Er betrachtet sie also als „benutzt“ und gibt sie nicht fei. Im englischen Sprachgebrauch wurde für solche Objekte der Begriff loitering objects geprägt. Die Referenz auf ein solches Objekt, das in der Programmlogik in Vergessenheit geraten ist, aber vom Gargabe Collector weiterhin beim Marking erreicht werden kann, nennt sich unwanted reference. Die Namensgebung ist wohl in beiden Fällen hinreichend selbsterklärend, sodass sie keiner weiteren Erläuterung bedarf.

Jetzt ist natürlich ein einzelnes loitering object nicht unbedingt ein wirklich relevantes Memory Leak. Zum diesem kommt es erst dadurch, dass der Vorgang des Objekterzeugens und -vergessens im Programm wiederholt wird. Mit der Zeit bleiben dann immer mehr Objekte über das Marking erreichbar, obwohl sie für die Programmlogik nicht mehr relevant sind. Die Folge ist, dass der Heap der JVM zu einem großen Teil aus „vergessenen“ Objekten besteht. Das wiederum kann dazu führen, dass die JVM zu einem späteren Zeitpunkt mit einem OutOfMemoryError abstürzt – nämlich dann, wenn der Heap völlig ausgeschöpft ist und kein weiterer Speicher mehr angefordert werden kann.

Beispiel für ein Memory Leak

Die wirklich entscheidende Frage haben wir bisher noch nicht angesprochen: Wie kann es sein, dass Objekte von der Programmlogik einfach vergessen werden? Dafür gibt es in der Praxis viele Gründe. Leider benötigt man zur Demonstration eines Memory Leaks ein Programmbeispiel mit einem gewissen Maß an Komplexität. Wenn das Beispiel zu einfach ist, fragt sich der Betrachter: „Wie kann man einen derart offensichtlichen Fehler machen?“ Andererseits wollen wir hier aus didaktischen Gründen auch nicht zu weit ausholen. Das folgende Beispiel ist in diesem Sinne ein Kompromiss: es ist nicht völlig trivial, sondern ein kleines bisschen komplex, aber nicht unbedingt in dem Ausmaß, wie man es in realen Softwareprojekten vorfindet. Wir haben als Beispiel die Implementierung des rudimentären Servers auf Basis der mit Java 7 eingeführten AsynchronousSocketChannels gewählt. Im Folgenden werden wir das Beispiel vorstellen und erläutern. Der Quellcode dazu ist unter [8] zu finden. Der Leser ist eingeladen, im Verlauf der Erläuterung nach dem Memory Leak Ausschau zu halten.

Client-Server-Kommunikation

Vorab eine kurze Erläuterung zum Prinzip der Client-Server-Kommunikation und der Architektur des Servers (Abb. 1.1): Ein Server bietet seinen Service an einem Server-Socket an. Der Client verbindet sich mit dem Server-Socket. Der Server akzeptiert den Client und erzeugt dabei intern einen neu...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang