Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
NEWSLETTER 
CONTACT 
Effective Java - Memory Leaks - Akkumulation

Effective Java - Memory Leaks - Akkumulation
Memory Leaks
Akkumulation
 

Java Magazin, Oktober 2012
Klaus Kreft & Angelika Langer

Dies ist die Überarbeitung eines Manuskripts für einen Artikel, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

Mit dem vorangegangenen Beitrag (/ ML1 /) haben wir die Reihe zum Thema Memory Leaks in Java gestartet.  Wir hatten uns die konkrete Implementierung eines Servers angesehen, der ein Memory Leak hatte, das zu einem OutOfMemoryError führen konnte.  Dieses Mal wollen wir detailliert die Mechanismen diskutieren, die allgemein ein Memory Leak und einen daraus resultierenden OutOfMemoryError verursachen.

Memory Leaks in Java

Als Beispiel für ein Memory Leak haben wir uns beim letzten Mal die Implementierung eines rudimentären Servers auf Basis der mit Java 7 eingeführten  AsynchronousSocketChannel s angesehen.  Das Memory Leak entstand dadurch, dass wir für jeden Client Verwaltungsinformation (die  ClientSession ) in einer Map gespeichert haben und diese clientspezifischen Map-Einträge nicht nach der Beendigung der Kommunikation mit den jeweiligen Clients wieder gelöscht haben.  Dadurch vergrößert sich die Map mit jedem neuen Client.  Erzeugt man dann in einem Testprogramm genügend viele Clients hintereinander, so stürzt der Server mit einem  OutOfMemoryError ab.  Der Sourcecode zu dem Beispiel sowie verschiedene alternative Korrekturen, die wir im letzten Artikel besprochen haben, finden sich hier (/ SRC /).

Ungewollte Referenzen

Rufen wir uns noch mal in Erinnerung, wie es genau zu einem Memory Leak kommt.  Der Garbage Collector ermittelt ausgehend von sogenannten Root References , welche Objekte in einem Java Programm referenziert und damit erreichbar sind.  Alle nicht erreichbaren Objekte räumt der Garbage Collector bei der Garbage Collection weg und gibt ihren Speicher frei.  Wenn wir nun auf ein Objekt verweisen, von dem wir sicher sagen können, dass wir es im weiteren Kontext unseres Programms gar nicht mehr benutzen werden, haben wir ein Memory Leak.  Denn das nicht mehr benötigte Objekt wird vom Garbage Collector nicht weggeräumt, weil es noch referenziert wird.  Diese Referenz wird in der englischsprachigen Fachliteratur unwanted reference (also: ungewollte Referenz) genannt.
Übertragen wir diese Beschreibung auf unser Memory-Leak-Beispiel vom letzen Artikel: Die client-spezifischen Verwaltungsdaten werden auch nach der Beendigung der Kommunikation mit dem Client weiter über die Map referenziert, so dass der Garbage Collector sie nicht freigeben kann.  Die Map mit ihrer internen Datenstruktur bildet also unsere ungewollte Referenz auf die client-spezifischen Daten. 

Akkumulation von Leaks führt zu  OutOfMemoryError

Eine ungewollte Referenz allein führt noch nicht zu einem problematischen Memory Leak.  Noch ein zweiter Aspekt ist wichtig, damit es zu einem  OutOfMemoryError aufgrund eines Memory Leaks kommt. Der Programm-Algorithmus muss so fehlerhaft sein, dass mit dem weiteren Ablauf immer mehr Objekte über ungewollte Referenzen erreichbar sind.  Das ist in unserem Beispiel gegeben: Immer wenn wir die Kommunikation mit einem Client beenden, "vergessen" wir dessen Verwaltungsdaten in der Map.  Die Anzahl der "geleakten" Objekte kann also beliebig anwachsen.
Typisch für Memory Leaks, die zu  OutOfMemoryError führen, ist der Umstand, dass sie nicht durch einzelne Referenzen wie Stackvariablen oder Felder von Referenztypen entstehen, sondern durch Akkumulation von Referenzen.  So kann eine einzelne Stackvariable von einem Referenztyp, die wir ganz zu Anfang in der  main -Methode angelegt haben, zwar eine ungewollte Referenz sein, aber sie führt nicht zu einem  OutOfMemoryError . Der Code für eine solche Situation kann zum Beispiel so aussehen:

 

    public static void main(String argv [] {

      String argMsg = "first argument: " + argv[0];

       System.out.println(arg Msg);        // Zeile 2

      // der Rest des Programms, das  noch lange laeuft

   }

Nach dem  println() in Zeile 2 wird der über  arg Msg referenzierte String nicht mehr genutzt. Die Referenz  arg Msg ist also eine ungewollte Referenz, weil sie ab einem bestimmten Zeitpunkt im Programmablauf auf ein Objekt zeigt, das nicht mehr genutzt wird.  Der von  arg Msg referenzierte String ist ein Memory Leak, weil die Stackvariable  arg Msg ihn bis zur Beendigung des  main -Threads (in unserem Fall: bis zum Ende des Programms) erreichbar hält.  Was aber einer Stackvariablen wie  arg Msg für einen  OutOfMemoryError fehlt, ist die Fähigkeit zur Akkumulation; die Anzahl der "geleakten" Objekte wächst nicht an. Ein ganz ähnliches Szenario ergibt sich, wenn nicht mehr benötigte Objekte über ein Feld von einem Referenztyp (statt einer Stackvariablen) am Leben erhalten werden; solange sie nicht akkumulieren, hat man zwar ungewollte Referenzen und Memory Leaks, aber keinen  OutOfMemoryError .
 

Bei einem Memory Leak, das zu einem  OutOfMemoryError führt, ist in der Regel irgendeine Art von Ressourcen-Verwaltung im Spiel. Diese Ressourcen-Verwaltung (oder genauer betrachtet deren interne Datenstruktur) bildet die ungewollte Referenz. In unserem Server-Beispiel aus dem vorangegangenen Artikel ist diese Ressourcen-Verwaltung die Map.  Deshalb ist es zur Vermeidung von Memory Leaks wichtig, dass die Verwaltung korrekt benutzt wird: Jeder Eintrag, der mit  put() in der Map abgelegt wird, muss irgendwann mit  remove() wieder gelöscht werden.
 

In gewisser Weise gibt es hier eine Analogie zu Memory Leaks in Programmiersprachen mit explizitem Speichermanagement wie zum Beispiel C++.  In C++ muss man darauf achten, dass jedes Objekt, das mit  new angelegt wird, auch mit  delete wieder freigegeben wird. In Java muss man darauf achten, dass Objekte, die an eine Verwaltung übergeben werden ( put() ), auch wieder gelöscht werden ( remove() ). 
 

Das Prinzip ist in beiden Fällen gleich: es geht um das korrekte Verwalten einer Ressource.  In C++ ist die fragliche Ressource der rohe Speicher, den man anfordert und freigibt.  Mit Speicherverwaltung auf diesem Niveau muss man sich in Java nicht befassen, weil der Garbage Collector die Speicherbereinigung erledigt.  Dennoch muss man sich in Java um die korrekte Verwaltung von Ressourcen kümmern.  Die fraglichen Ressourcen sind dabei Java-Objekte, die man in eine Verwaltungsstruktur einhängt und später wieder aushängen muss, damit sie unerreichbar und „garbage collectible“ werden. 

Die Dualität der Ressourcen-Verwaltung

Es geht ganz allgemein um das duale Prinzip des Ein- und Aushängen (oder „acquire“ / „release“ oder „attach“ / „detach“ oder Zugänglichmachen und Freigeben) von Ressourcen.  Dieses duale Prinzip gibt es auch an anderen Stellen in Java, zum Beispiel beim „lock“ / „unlock“ von Locks oder beim „open“ / „close“ von Streams oder Channels.  Im Zusammenhang mit dem „new“ / „delete“ von Speicher hat uns der Garbage Collector von der Bürde des „delete“ befreit.  Die Dualität an sich bleibt aber erhalten.  Diese Dualität zu beachten ist immer dann ganz besonders wichtig, wenn wiederholt und regelmäßig Objekte in langlebige Verwaltungsstrukturen eingehängt werden.  Wenn dort das Aushängen vergessen wird, kommt es zur Akkumulation von ungewollten Referenzen, Memory Leaks und letztlich zum  OutOfMemoryError
Nun haben wir es bislang so dargestellt, als gäbe es nur die Möglichkeit, eine Ressourcen-Verwaltung richtig (man löscht) oder falsch (man löscht nicht) zu benutzen.  Es gibt aber auch dabei eine Grauzone.  Zum Beispiel, wenn man die Verwaltung im Allgemeinen richtig nutzt und nur in einem speziellen Zweig des Programms das Löschen vergessen wird.  Wenn dieser Fehler während des konkreten Programmablaufs nicht oder nur sporadisch auftritt, entstehen entweder gar keine Memory Leaks oder der 'geleakte' Speicher ist von seinem Umfang so gering, dass es zu keinem  OutOfMemoryError mit anschließendem Programmabsturz kommt.

Ein weiters Beispiel: Callbacks

Schauen wir uns ein weiteres Beispiel für ein typisches Memory Leaks in Java an und versuchen wir, das bisher Gesagte darauf zu übertragen.

Bei dem Beispiel geht es um Callbacks.  Bekanntermaßen werden Callbacks in Java als Klassen erstellt, die das jeweils spezifische Callback-Interface implementieren.  Ein Objekt dieser Klasse wird dann in den Callback-Emitter eingehängt.  Dieses Callback-Objekt empfängt und verarbeitet die Callback-Ereignisse. [1]

 
[1] Mit Java 8 und seinen Lambda-Ausdrücken wird dieses Modell in Zukunft einige Ergänzungen erfahren.  Wir werden aber in diesem Artikel nicht auf diese Neuerungen eingehen.

Ein Beispiel für Callbacks im JDK sind die AWT Event Listener.  Schauen wir uns einmal ganz konkret den  java.awt.event.ActionListener des  java.awt.Button an.  Man muss mit einer konkreten Klasse das  ActionListener -Interface (das heißt, deren Methode  actionPerformed () ) implementieren und dann eine Instanz dieser Klasse als Callback mit der Methode  addActionListener() beim  Button -Objekt registrieren. Intern werden die registrierten Callback-Objekte mit Hilfe des  java.awt.AWTEventMulticaster als verkette Liste gespeichert.  Wenn ein Event aufgetreten ist, werden die für diesen Event registrierten Callbacks mit dem Event als Parameter der Reihe nach, wie sie in der Liste verkettet sind, aufgerufen.
 

Soweit die Grundzüge des AWT Event Callback Mechanismus.  Wie kann es dabei jetzt zu einem Memory Leaks kommen?
 

Ganz einfach:  Bei ungeschicktem Programmdesign kann es vorkommen, dass man immer wieder neue Listener-Callback-Objekte in die Liste einhängt, ohne die alten mit  removeActionListener() zu entfernen.  Die "vergessen" Callback-Objekte akkumulieren sich, da die verkettete List der Callback-Objekte immer länger werden kann.  Wir haben also eine ähnliche Situation wie bei der Map im Serverbeispiel.  Wir müssen die Ressourcen-Verwaltung, die hinter dem Callback-Mechanismus steckt, richtig bedienen  Wir müssen die Dualität der Ressourcen-Verwaltung beachten und die Callback-Objekte nicht nur einhängen, sondern auch wieder aushängen, wenn wir sie nicht mehr brauchen.
 

Dass eine Callback-Verwaltung auch anders, das heißt ohne explizites Aushängen, funktionieren kann, haben wir in unserem Serverbeispiel des letzten Artikels gesehen.  Die Callbacks bei den  AsynchronousSocketChannel s bleiben nur für einen einzigen Aufruf des Callbacks eingehängt; sie werden nicht dauerhaft in internen Datenstrukturen gespeichert. Das hat natürlich den Nachteil, dass sich der Callback im Normalfall immer wieder selbst einhängen muss.  Der Vorteil ist jedoch, dass der Callback nicht explizit ausgehängt werden muss.
 

Fazit: Man muss die Ressourcen-Verwaltung, die man benutzt kennen, verstehen und korrekt benutzen, um Memory Leaks zu vermeiden.

Callbacks und Non-static Inner Classes

Kommen wir noch mal auf die Callback-Objekte zurück, die sich bei einem Event-Callback-Mechanismus akkumulieren können.  Hier wird bisweilen argumentiert, dass die einzelnen Callback-Objekte nicht besonders groß seien und damit keine wirkliche Memory Leak Problematik vorliegt, die zu Speicherengpässen führen kann.  Bei einer solchen Argumentation wird jedoch vergessen, dass Callbacks meist als Non-Static Inner Classes implementiert werden.  Das biete sich an, weil man in dem jeweiligen Kontext meist eine Klasse braucht,
  • die das Callback-Interface (zum Beispiel  ActionListener ) implementiert und
  • gleichzeitig auch Zugriff auf die Felder der äußeren umgebenden Klasse hat.
  • Genau für solche Situationen sind die Non-Static Inner Classes in Java eingeführt worden; man wollte damit die Implementierung von Callbacks einfacher machen, als es bis dahin in Java der Fall war. 
    Wichtig für unsere Überlegungen ist nun, dass eine Instanz einer Non-Static Inner Class (also unser Callback-Objekt) eine Referenz auf das assoziierte Objekt der äußeren umgebenden Klasse hält. Häufig wird diese Referenz als verborgen (im Englischen: hidden ) bezeichnet wird. Sie kann aber mit < outerClassName>.this explizit angesprochen werden.  Diese „hidden“ Referenz führt dazu, dass nicht nur das eingehängte Callback-Objekt selbst, sondern auch das assoziierte Objekt der äußeren Klasse (eben über diese Referenz) erreichbar bleibt.  Das heißt, das Leak besteht nicht nur aus den "vergessenen" Callback-Objekten (die unter Umständen wirklich nicht so groß sind), sondern zusätzlich auch aus den mit ihnen assoziierten Objekten der äußeren Klasse (die ihrerseits möglicherweise noch eine ganze Menge anderer Objekte referenzieren).

    OutOfMemoryError   bedeutet nicht (immer) Memory Leak

    Damit haben wir die beiden Kernaspekte eines Memory Leaks ausführlich diskutiert, nämlich:
  • die ungewollte Referenz, die zum Speicherverlust (genauer gesagt, zur fehlenden Speicherfreigabe) führt, und
  • die Akkumulation des verlorenem Speicher, die zum Speicherengpass und damit zum  OutOfMemoryError führt.
  • Im nächsten Artikel werden wir uns ungewollten Referenzen widmen, die nicht zu Memory Leaks führen, aber dennoch stören. 
     

    Hier noch eine abschließende Bemerkung:  Bisweilen wird unterstellt, dass jeder  OutOfMemoryError auf einem Memory Leak, also auf ungewollten Referenzen, basiert.  Das ist jedoch nicht immer der Fall.  Manchmal wird der Speicherbedarf der Applikation einfach zu groß. 
     

    Das kann man an dem Beispiel aus dem letzten Artikel sehen.  Betrachten wir das Programm in einer seiner korrigierten Versionen, wo es keine ungewollten Referenzen mehr gibt und alle clientspezifischen Verwaltungsdaten korrekt bei Beendigung der Kommunikation mit dem Client ausgehängt und „garbage collectible“ werden.  Auch ohne ungewollte Referenzen und Memory Leaks kann es zu einem  OutOfMemoryError kommen, nämlich dann, wenn sehr viele Clients gleichzeitig mit dem Server verbunden sind und damit sehr viele Verwaltungsdaten (Objekte vom Typ  ClientSession ) im Speicher gehalten werden müssen. Dann sieht die Situation beim  OutOfMemoryError so ähnlich aus wie im Fehlerfalle mit dem Memory Leak: man wird unzählige Objekte vom Typ  ClientSession  im Speicher vorfinden.  Diese Objekte sind aber nicht ungewollt referenziert, sondern sie werden alle gebraucht.  Die Applikation verschwendet keinen Speicher, sondern sie ist wirklich so groß und braucht bei weiteren Clients auch noch mehr Speicher. 
     

    Wenn man sich unsicher ist, was der Grund für den  OutOfMemoryError war, muss man die Ursachen genauer analysieren.  Wie man dabei vorgeht, wollen wir uns in einem zukünftigen Artikel ansehen. 

    Zusammenfassung und Ausblick

    Wir haben uns angesehen, wie ungewollte Referenzen Memory Leaks erzeugen und die Akkumulation von Memory Leaks zum Abbruch des Programms mit  OutOfMemoryError führen kann.  Die Akkumulation von Leaks entsteht dadurch, dass wir Abstraktionen nutzen, die eine explizite Freigabe verlangen.  Beispiel für solche Abstraktionen sind im einfachsten Fall Collections,  wie die  Map in unserem Beispiel vom letzten Artikel.  Aber auch andere Ressource-Verwaltungen besonders in Frameworks, wie etwa das Callback-Management der AWT-Komponenten in dem Beispiel in diesem Artikel, fallen darunter.  In solchen Situationen ist es wichtig, dass unser Programmdesign die Anforderungen der Ressourcen-Verwaltung berücksichtigt und die explizite Freigabe in allen Fällen korrekt erfolgt. 
    Beim nächsten Mal wollen wir uns Memory Leaks ansehen, die sich nicht akkumulieren und damit in der Größe beschränkt bleiben.  Auch wenn sie nicht zu Programmabbrüchen auf Grund von  OutOfMemoryError führen, sind sie auf ihre eigene Weise interessant.

    Literaturverweise

    /ML1/
    Memory Leaks  Teil 1: Ein Beispiel
    Klaus Kreft, Angelika Langer, Java Magazin, August 2012
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/64.Mem.Leaks/64.Mem.Leaks.html 
    /SRC/
    Sourcecode für das Memory-Leak-Beispiel aus diesem Artikel
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/66.Mem.Analysis/66.Mem.Analysis.zip

     

    Die gesamte Serie über Memory Leaks:

    /MEMLKS-1/ Memory Leaks - Ein Beispiel
    Klaus Kreft & Angelika Langer, Java Magazin, August 2012
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/64.Mem.Leaks/64.Mem.Leaks.html
    /MEMLKS-2/ Akkumulation von Memory Leaks
    Klaus Kreft & Angelika Langer, Java Magazin, Oktober 2012
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/65.Mem.Akkumulation/65.Mem.Akkumulation.html
    /MEMLKS-3/ Memory Leaks - Referenzen "ausnullen"
    Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2012
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/66.Mem.NullOut/66.Mem.NullOut.html
    /MEMLKS-4/ Tools für die dynamisch Memory Leak Analyse
    Klaus Kreft & Angelika Langer, Java Magazin, Februar 2013
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/67.MemLeak.ToolCyclic/67.MemLeak.ToolCyclic.html
    /MEMLKS-5/ Heap Dump Analyse
    Klaus Kreft & Angelika Langer, Java Magazin, April 2013
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/68.MemLeak.ToolDump/68.MemLeak.ToolDump.html
    /MEMLKS-6/ Weak References
    Klaus Kreft & Angelika Langer, Java Magazin, Juni 2013
    URL: http://www.angelikalanger.com/Articles/EffectiveJava/69.MemLeak.WeakRefs/69.MemLeak.WeakRefs.html

     
     

    If you are interested to hear more about this and related topics you might want to check out the following seminar:
    Seminar
     
    Effective Java - Advanced Java Programming Idioms 
    4 day seminar ( open enrollment and on-site)
    High-Performance Java - Profiling and Tuning Java Applications
    4 day seminar ( open enrollment and on-site)
     

     
      © Copyright 1995-2016 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/65.Mem.Akkumulation/65.Mem.Akkumulation.html  last update: 29 Nov 2016