Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | 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 
CONTACT 
Effective Java - Memory Leaks - Ein Beispiel

Effective Java - Memory Leaks - Ein Beispiel
Memory Leaks
Ein Beispiel
 

Java Magazin, August 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 ).

 

Unsere Reihe über Java 7 Neuerungen haben wir mit dem letzten Artikel abgeschlossen.  Da sich Oracle mit Java 8 noch ein wenig Zeit lässt (als anvisierter Freigabe-Termin wird im Augenblick „Sommer 2013“ genannt), wollen wir die Gelegenheit nutzten, um noch einmal an die Artikelserie über Garbage Collection aus den Jahren 2010/2011 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 (/ ARGC /) oder zusammengefasst als Buch (/ JCP /) 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.

Was ist in Java ein Memory Leak?

Bei der Erwähnung von Memory Leaks mag sich der eine oder andere Leser fragen, was denn das sein soll – ein Memory Leak in Java.  Gibt es so etwas in Java ü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 allemal 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++. [1] Der Nachteil ist, dass Memory Leaks in Java meist keine Flüchtigkeitsfehler bei der Programmierung sind, sondern häufig eher Konzept- bzw. Designfehler.
  [1] 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 (/ CAM /), um ihre Fähigkeiten unter Beweis zu stellen.

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 wird.  Wie macht er das?  Bei allen Sun/Oracle Garbage Collectoren (außer dem Garbage First („G1“) Collector, siehe / GC4 /) wird dazu ein Marking angewandt.  Vereinfacht beschrieben sieht das Marking so aus, dass ausgehend von den sogenannten 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 nichtmarkierten 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 hier (/ GC1 /, / GC2 /, / GC3 /).
Die Root References sind, wie der Name schon andeutet, die Ausgangsreferenzen zu allen Objekten in einer Java Virtual Machine (JVM).  Beispiele für Root References sind die Stackpointer zu allen Thread-Stacks.  Variablen von Referenztypen, die als Methoden-Parameter 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 frei. 
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, so dass sie keiner weiteren Erläuterung bedarf.
Jetzt ist natürlich ein einzelnes loitering object nicht unbedingt ein wirklich relevantes Memory Leak.  Zum Memory Leak 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. 

Ein 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.  Wir wollen uns hier ein konkretes Beispiel ansehen.
Leider benötigt man zur Demonstration eines Memory Leaks ein Programmbeispiel mit einem gewissen Maß an Komplexität.  Wenn das Beispiel zu einfach ist, dann fragt sich der Betrachter: „Wie kann man einen derart offensichtlichen Fehler machen?“ Andererseits wollen wir hier aus didaktischen Gründen auch nicht allzu 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 es rudimentären Servers auf Basis der mit Java 7 eingeführten  AsynchronousSocketChannel s gewählt.
Im Folgenden werden wir das Beispiel vorstellen und erläutern.  Der Sourcecode dazu ist übrigens unter / SRC / zu finden.  Der Leser ist eingeladen, im Verlauf der Erläuterung nach dem Memory Leak Ausschau zu halten.

Vorbemerkung: Client-Server-Kommunikation

Vorab eine kurze Erläuterung zum Prinzip der Client-Server-Kommunikation und der Architektur des Servers (siehe Abbildung 1):  Ein Server bietet seinen Service an einem Serversocket an. Der Client verbindet sich mit dem Serversocket. Der Server akzeptiert den Client und erzeugt dabei intern einen neuen Socket, über den dann der Datenaustausch mit dem Client stattfindet.  Wir wollen uns in unserem rudimentären Server auf das Empfangen von Client-Daten beschränken; zurücksenden wollen wir nichts.  Deshalb hat der Server zwei wesentliche Aktionen auszuführen: das Akzeptieren von neuen Clients ( accept ) und das Empfangen von Client-Daten ( read ).
Abbildung 1: Client-Server-Kommunikation

 
 

Wenn man Asynchronous Channels verwendet, geht es im Prinzip genauso.  Anstelle des Serversocket benutzt man einen  AsynchronousServerSocketChannel und statt des Socket einen  AsynchronousSocketChannel .  Statt die  accept - und  read -Aktionen synchron zu machen, werden sie asynchron abgearbeitet von Callbacks, die in die Channels eingehängt werden, und vom Framework aufgerufen werden, sobald Ereignisse/Daten am Channel anliegen.
 

Der Vorteil der Asynchronous Channels gegenüber den „normalen“ Sockets ist, dass der Server nicht mit synchronen Methodenaufrufen der  Socket bzw.  ServerSocket -Klasse auf die Aktionen des Clients warten muss.  So muss der Server beispielsweise nicht darauf warten, dass ein Client sich am Serversocket meldet, um akzeptiert zu werden; er muss auch nicht auf Daten warten, die vom Client gesendet werden. Die Asynchronous Channels erlauben es, auf diese Ereignisse mit Callbacks zu reagieren.  Dabei nimmt einem der Asynchronous-Channel-Framework viel Arbeit ab und es ergibt sich trotz Asynchronität ein relativ einfaches Programmiermodell für den Server.  Weitere Details zu den Asynchronous Channels finden sich in einem unserer zurückliegenden Artikel über die NIO2 Erweiterungen in Java 7 (/ NIO2 /).
 

Schauen wir uns nun die Implementierung an. Den Client-Code werden wir nicht betrachten, weil unser Memory Leak im Server entsteht.  Wir konzentrieren uns deshalb allein auf die Server-Implementierung. Der gesamte Sourcecode inklusive Test-Clients ist aber unter / SRC / zu finden. 
 

Die Server-Konstruktion

Die  Server -Klasse hat zwei Methoden: einen Konstruktor und eine public Methode  doAccepting() . Der Konstruktor sieht dabei folgendermaßen aus:
public  Server() throws IOException {

   serverSocket Channel = AsynchronousServ erSocketChannel.open();

serverSocket Channel .bind(new InetSocketAddress(SERVER_PORT));

}
 
 

Der Konstruktor öffnet den  AsynchronousServerSocketChannel am  SERVER_PORT .  Damit steht der Serversocket  bereit, mit dem sich  der Client verbindet und an dem der Server Clients akzeptiert. 
 

Accept-Callback einhängen

Für das Akzeptieren wird ein   accept - Callback eingehängt.  Dafür ist die  doAccepting() -Methode des Servers zuständig.  Sie hängt einen  accept -Callback vom Typ  CompletionHan dler<AsynchronousSocketChannel, Objekt> in den  Asynchronous S erverSocketChannel ein. Dieser Callback behandelt nun das Akzeptieren der Clients. Die  doAccepting() -Methode sieht so aus:
public void doAccepting() {

   serverSocketChannel. accept( null,                // Zeile 1

      new CompletionHandler<AsynchronousSocketChannel,Object>() { /*  …  later */ } );    // Zeile  2

}
 
 

Die Implementierung des Callbacks (siehe // Zeile 2) als Anonymous Inner Class schauen wir uns gleich genauer an.  Vorher noch eine Bemerkung zur Semantik der Callbacks bei Asynchronous Channels:  jeder eingehängte Callback bleibt nur so lange aktiv, bis er aufgerufen wurde.  Das heißt, er muss immer wieder neu eingehängt werden - am Besten gleich als letztes Statement des Callbacks selbst.  Und genau so werden wir es später implementieren (siehe z.B. // Zeile 11).
 

Der eingehängte Accept-Callback

Für unseren  accept - Callback vom Typ  CompletionHan dler<AsynchronousSocketChannel, Objekt> (siehe // Zeile 2) müssen zwei Methoden implementiert werden:
 
 

public void completed(AsynchronousSocketChannel channel, Object attachment) // Zeile  3

public void fail ed (Throwable t, Object attachment)                            // Zeile  4
 
 

Die erste Methode  completed() (siehe // Zeile 3) wird aufgerufen, wenn das Akzeptieren eines Clients funktioniert. Bei einem erfolgreichen Akzeptieren wird ein neuer Socket geöffnet, über den anschließend die weitere Kommunikation mit dem Client geht, der gerade akzeptiert wurde.  Bei unserer asynchronen Arbeitsweise über Callbacks wird der neue Socket vom Typ  AsynchronousSocketChannel als erster Parameter der  completed() -Methode des  accept -Callbacks zur Verfügung gestellt.  Auf den zweiten Parameter kommen wir gleich noch zu sprechen.
 

Die zweite Methode  fail ed () (siehe // Zeile 4) wird im Fehlerfall aufgerufen.  Ihr erster Parameter vom Typ  Throwable beschreibt die genaueren Umstände des Fehlers.  Auf die Fehlerbehandlung wollen wir hier nicht weiter eingehen.  Für unser Problem - das Memory Leak - ist die Fehlerbehandlung nicht relevant.
 

Der Vollständigkeit halber sei noch erwähnt, dass der zweite Parameter beider Methoden (d.h. das  attachment ) das Objekt ist, das oben als erster Parameter beim Einhängen des Callbacks mit  accept() (siehe // Zeile 1) mitgegeben wurde.  Dieses Objekt wird beim Ausführen des Callbacks dann wieder als zweiter Parameter den Callback-Methoden mitgegeben.  Wir haben für dieses Durchschleusen in unserer Implementierung keine Verwendung, deshalb haben wir beim  accept() als ersten Parameter  null übergeben (siehe // Zeile 1).  Das bedeutet, wir können diesen Parameter im Callback ignorieren.
 

Die Implementierung des eingehängten Accept-Callback

Kommen wir also nun zur Implementierung der  completed() -Methode aus // Zeile 3.
 
 

public void completed(AsynchronousSocketChannel channel, Object attachment) { // Zeile  5

   ClientSession session = new ClientSession();                                   // Zeile  6

   sessionToChannel.put(session, channel);                                       // Zeile  7

   final ByteBuffer buf = ByteBuffer.allocateDirect(256);                        // Zeile  8                

   channel.read(buf, session,                                                     // Zeile  9

                new CompletionHandler<Integer, ClientSession>()                  // Zeile  10                

                { /*  … later … */  });

   serverSocketChannel.accept(null, this);                                       // Zeile  1 1                

}
 
 

Als erstes erzeugen wir uns ein  ClientSession -Objekt für den neuen Client (siehe // Zeile 6).  Wie die Klasse  ClientSession genau aussieht, schauen wir uns gleich weiter unten an.  Die Client-Session ist dafür zuständig, die vom Client empfangenen Daten zu verarbeiten.
 

Dann merken wir uns in einer Map die Beziehung zwischen session und  channel (siehe // Zeile 7).  Diese Information brauchen wir später bei der Implementierung des  read -Callbacks, der das Empfangen der Clientdaten behandelt.  Die Map  sessionToChannel ist ein Attribut der  Server -Klasse, das so definiert ist:
 

private final Map<ClientSession, AsynchronousSocketChannel> sessionToChannel = 
                                new ConcurrentHashMap<ClientSession, AsynchronousSocketChannel>();


 

Unsere Map ist vom Type  ConcurrentHashMap, damit die Callbacks von verschiedenen Clients problemlos konkurrierend auf sie zugreifen können.
 

Wie oben bereits erwähnt, wollen wir in unserem einfachen Beispiel nur Daten vom Client zum Server senden; die umgekehrte Richtung vom Server zum Client wollen wir nicht implementieren.  Das heißt, was uns nun noch im Server fehlt, ist die Funktionalität für das Empfangen der Clientdaten ( read ).  Das geschieht in // Zeile 8 und // Zeile 9.
 

Dazu erzeugen wir uns einen  ByteBuffer , in den die empfangenen Daten geschrieben werden sollen (siehe // Zeile 8). 
 

Wir hängen diesen  ByteBuffer zusammen mit der session und dem  read -Callback, der beim Empfangen der Daten ausgeführt werden soll, in den  channel zum Client ein; das geschieht über den Aufruf der Methode  read() (siehe // Zeile 9).  Die Implementierung des  read -Callbacks (siehe // Zeile 10) als Anonymous Inner Class schauen wir uns weiter unten genauer an. 
 

Der zweite Parameter des  read() , also die  session , wird nicht von der  completed() -Methode selbst genutzt, sondern durchgeschleust und beim Aufruf des  read -Callbacks wieder übergeben.  Wie wir weiter unten sehen werden, brauchen wir sie dort (siehe // Zeile 13 und // Zeile 16).  Dieses Durchschleusen eines Objekts vom Einhängen des Callbacks bis zum Ausführen des Callbacks hatten wir uns oben bei dem  accept- Callback schon mal angesehen (siehe // Zeile 1),  dort aber nicht genutzt, sondern stattdessen nur  null übergeben; hier beim  read -Callback brauchen wir das Durchschleusen aber.
 

Wie schon oben erwähnt, bleibt der  accept -Callback nur so lange aktiv, bis er aufgerufen wurde.  Das heißt, er muss immer wieder neu eingehängt werden, damit weiterhin neue Clients akzeptiert werden.  Dieses Wiedereinhängen machen wir als letzte Aktion in der completed() -Methode (siehe // Zeile 11).  Wir nehmen einfach den Callback wieder, der gerade ausgeführt wird.  Das heißt der zweite Parameter im erneuten  accept() -Aufruf ist  this und der erste ist  null wie schon oben in // Zeile 1, weil wir kein Objekt an den  accept -Callback durchschleusen wollen. 
 

Die Client-Session

Sehen wir uns nun die Client-Session genauer an; wir hatten ein  ClientSession -Objekt im  accept -Callback erzeugt (siehe // Zeile 6) und verwendet.  Die Klasse ClientSession sieht wie folgt aus:

 
 

class ClientSession {

  private static final AtomicInteger clientCnt = new AtomicInteger(1);

  private final int myId = clientCnt.getAndIncrement();

  private volatile int byteCnt;

  public boolean handleInput(ByteBuffer buf, int len) {    // Zeile  1 2               

    if (len >= 0) {

      byteCnt += len;

      return true;

    } else {

      System.out.println("received " + byteCnt + " bytes from client " + myId);

       return false;

     }

   }

}
 
 

In unserem einfachen Server-Beispiel simuliert diese Klasse eigentlich nur die Funktionalität, die typischerweise in einer Client-Session angesiedelt ist.  Zum Beispiel kann man sich vorstellen, dass in einem echten Server, bei dem die Kommunikation aus XML-Dokumenten besteht, die empfangenen Daten in der Session gespeichert werden, bis sie ein vollständiges Dokument ergeben, das geparst und weiterverarbeitet werden kann. 
 

Die zentrale (und einzige) Methode unserer Client-Session ist die  handleInput() -Methode (beginnend in // Zeile 12).  In unserem einfachen Fall wird in der  handleInput() -Methode nur die Anzahl der vom Client empfangenen Bytes gezählt, solange bis der Client die Verbindung beendet.   Dann wird die Anzahl zusammen mit der Client-Session-ID ausgedruckt. Der Returnwert der  handleInput() -Methode ist  true , wenn weiterer Input erwartet wird, und  false , wenn die Verbindung beendet ist. Die Methode  handleInput() arbeitet sehr eng mit dem  read -Callback zusammen, der das Empfangen der Daten abhandelt. 
 

Der eingehängte Read-Callback

Kommen wir deshalb nun zum  read - Callback vom Typ CompletionHandler<Integer, ClientSession> , den wir in // Zeile 10 eingehängt hatten.  Wie vorher auch bei dem  accept - Callback sind die zwei Methoden  completed() und  failed() zu implementieren.  Wir wollen auch hier der Devise folgen „Besser keine Fehlerbehandlung zeigen als eine rudimentäre.“ und schauen uns deshalb nur den Code von completed() an:
public void completed(Integer len, ClientSession  session) {

  AsynchronousSocketChannel channel = sessionToChannel.get( session); // Zeile  13                

  if ( session.handleInput(buf, len)) {                                // Zeile 14                

    buf.clear();                                                        // Zeile 15                

     channel.read(buf,  session, this);                                // Zeile  16                

   }

  else {

      try { channel.close();                                           // Zeile  17                

     catch (IOException e) { /* ignore */ }

   }

}

Die Parameter der Methode sind  len , die Anzahl der Bytes, die vom Client empfangen wurden, und die  ClientSession , die hierhin vom  read( ) -Aufruf (in // Zeile 9) durchgeschleust wurde.  Wir holen uns als erstes den zur  session korrespondierenden  channel aus unserer  sessionToChannel -Map (siehe // Zeile 13). 
 

Dann rufen wir auf der  session die  handleInput() -Method auf und übergeben dabei den  ByteBuffer buf mit den Inputdaten sowie die Anzahl der Bytes, die in den  ByteBuffer gelesen wurden (siehe // Zeile 14).  Wir haben Zugriff auf den Puffer  buf , weil er im äußeren Scope (dem der  completed( ) -Methode des  accept - Callbacks) final deklariert war (siehe // Zeile 8) und wir in der inneren Klasse (der Implementierung des des  read- Callbacks) Zugriff auf die final Variablen des äußeren Scopes haben.

 
Der Returnwert von  handleInput() bestimmt, ob wir weiter Daten vom Client lesen oder die Verbindung zum Client beenden sollen.  Ein kurzer Blick auf die Implementierung von  handleInput() oben (beginnend in // Zeile 12) zeigt, dass die Verbindung zum Client beendet wird, wenn eine negative Anzahl Bytes vom Client empfangen wurde.  Das macht Sinn, denn wenn man in die Javadoc schaut, wird man finden, dass  len == -1   das end-of-file bedeutet, in unserem Fall also, dass der Client die Verbindung beendet hat.

 

Der Code des  if - und  else -Zweigs ist relativ übersichtlich.  Im Fall von „Weiterlesen“ ( if -Zweig) setzten wir den ByteBuffer mit  clear() zurück (siehe // Zeile 14), um ihn anschließend im  rea d- Callback wieder zu verwenden (siehe // Zeile 16).  Wie oben im  ac cept- Callback setzeen wir wieder den Callback, den wir gerade durchlaufen, als Callback in den  channel ein.  Der dritte Parameter von  read() ist deshalb  this
 

Im Fall von „Verbindung beenden“ ( else -Zweig) rufen wir die  close() Methode auf dem  channel auf (siehe // Zeile 17).  Wir müssen dies in einem  try -Block tun, da  close() eine  IOException werfen kann.  Eine Behandlung der Exception implementieren wir nicht.  Was soll man auch tun, wenn ein  close()   nicht fehlerfrei funktioniert?  Wiederholen?  Wie häufig?
 

Ohnehin, an der fehlenden Behandlung der  IOException hier liegt das Memory Leak nicht.  Auf das Memory Leak können wir nämlich jetzt zurückkommen.  Wir haben uns nun den gesamten Code des Servers angesehen.  Wo haben wir Objekte vergessen, die den Speicherverbrauch kontinuierlich anwachsen lassen?

Wo ist das Memory Leak?

Da der gesamte Code des Servers recht übersichtlich ist, ist das Memory Leak noch relativ einfach zu finden. Das Memory-Leak entsteht, weil wir für jeden Client einen Eintrag in der  sessionToChannel -Map anlegen (siehe // Zeile 7).  Wir löschen diesen Eintrag aber nicht,  wenn wir mit dem  close() die Verbindung zum Client endgültig beenden (siehe // Zeile 17).  Damit bleiben sowohl das  ClientSession -Objekt ( Key des Eintrags) als auch das  AsynchronousSocketChannel -Objekt ( Value des Eintrags) weiter referenziert und keines der Objekte kann vom Garbage Collector weggeräumt werden.  Bei jedem Client, mit dem wir nicht mehr kommunizieren, wachsen also in der  sessionToChannel -Map die vergessenen Objekte an.  Das ist ein bisschen so, als hätten wir in einer Sprache wie C/C++ vergessen, unseren Heap-Speicher mit free() bzw.  delete () wieder frei zu geben.  Deshalb ist die Bezeichnung „Memory Leak“ auch in einer Sprache wie Java irgendwie gerechtfertigt.

Korrektur: Eliminierung des Memory Leaks

Für die Korrektur gibt es mehrere Möglichkeiten. 
Lösung #1:
Wir müssen beim endgültigen Beenden der Kommunikation mit einem Client den korrespondierenden Eintrag in der Map löschen.  Die korrigierte  completed( ) -Methode des  read -Callbacks müsste also so aussehen:
public void completed(Integer len, ClientSession  s ession) {

   AsynchronousSocketChannel channel = sessionToChannel.get( s ession);

   if (session.handleInput(buf, len)) {

    buf.clear();

    channel.read(buf, session, this);

   }

  else {

     sessionToChannel.remove(session);

      try { channel.close(); } 

     catch (IOException e) { /* ignore */ }

   }

}
 
 

Diese Korrektur ist zwar naheliegend, aber nicht unbedingt die im Gesamtkontext schlüssigste.  Sinnvoll wäre eine Lösung, bei der man den Map-Eintrag und damit die Map nicht braucht, so dass man das Löschen des Map-Eintrags gar nicht erst vergessen kann.

Lösung #2:
Die einfachste Lösung sieht so aus, dass man die Map weglässt und in der  completed() -Methode des  accept -Callbacks den ersten Parameter vom Typ  AsynchronousSocketChannel als  final deklariert (siehe // Zeile 5):

 

public void completed( final AsynchronousSocketChannel channel, Object attachment) // Zeile 5

   ... 

}
 

Da der  read - Callback als Anonymous Inner Class im Scope dieser Methode implementiert ist, kann er so direkt auf den  channel zugreifen und braucht ihn sich nicht aus der Map zu holen.  Diese Lösung wollen wir an dieser Stelle nicht in allen Details zeigen.  Sie ist im Sourcecode unter /SRC/ zu finden. 

Lösung #3:
Wem diese Lösung mit der final-Deklaration und der anonymen inneren Klasse zu sehr nach „glücklicher Fügung“ aussieht, dem können wir noch eine dritte Variante anbieten. Auch sie verzichtet auf die Map.  Gegenüber der vorherigen Lösung lässt sie sich aber deutlich besser verallgemeinern. 
Wenn zwischen der  session und dem  channel eine so enge Beziehung besteht, dass wir beide über einen Map-Eintrag verbinden wollen, so können wir doch alternativ auf die Map verzichten und den  channel zu einem Attribut der  session machen. Der  channel würde dann im Konstruktor der  ClientSession gesetzt.  Da die  session beim  read - Callback durchgeschleust wird, muss man sie nur noch um einen Getter auf das neue  channel - Attribut erweitern.  So steht dann zusammen mit der  session immer auch der  channel im  read - Callback Verfügung. Auch diese Lösung wollen wir hier nicht im Detail zeigen.  Sie ist auch im Sourcecode unter /SRC/ zu finden. 

Fazit

Memory Leaks können bereits in relativ kurzen Programmen passieren.  Sie sind selten offensichtlich und passieren häufig bei der Benutzung von Frameworks oder „fremden“ Abstraktionen, wo man sich u.U. nicht ganz klar darüber ist, wer welche Objekte wie lange benötigt.  Im diskutierten Beispiel hätten wir beachten müssen, dass mit dem Ende einer Client-Verbindung sowohl die Client-Session als auch der benutzte Channel nicht mehr benötigt werden und dass sie deshalb explizit unerreichbar gemacht werden müssen, damit sie vom Garbage Collector weggeräumt werden können.  Man darf Referenzen, wie wir sie in der Map gehalten haben, in solchen Situationen nicht vergessen.

Zusammenfassung und Ausblick

In diesem Artikel haben wir eine Reihe zum Thema „Memory Leaks in Java“ begonnen.  Wir haben gesehen, dass Memory Leaks durch im Programm „vergessene“ Objekte entstehen, die der Garbage Collector aber weiterhin als erreichbar identifiziert und deshalb nicht wegräumt.  An der Implementierung unseres Servers haben wir gesehen, wie so ein Memory Leak konkret aussehen kann.  Das Beispiel ist durchaus typisch.  In einem relativ komplexen Kontext vergisst man, Referenzen auf Objekte aufzulösen, die im weiteren Ablauf des Programms nicht mehr benötigt werden. 
In den folgenden Artikeln wollen wir uns weitere Themen in diesem Umfeld ansehen, darunter:
  • weitere typische Memory Leak Situationen,
  • allgemeine Lösungsansätze für Leaks (z.B.  WeakReferenc es), und
  • Techniken, wie man Memory Leaks suchen kann .
  • Verweise

    /ARGC/
    Klaus Kreft, Angelika Langer: Artikelserie zur Garbage Collection
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava.html 
    /CAM/ 
    Creating a memory leak with Java
    URL: http://stackoverflow.com/questions/6470651/creating-a-memory-leak-with-java
    /GC1/  
    Garbage Collection  Teil 2: Young Generation Garbage Collection
    Klaus Kreft, Angelika Langer, Java Magazin, April 2010
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/50.GC.YoungGenGC/50.GC.YoungGenGC.html
    /GC2/ 
    Garbage Collection Teil 3: Old Generation Garbage Collection - Mark And Compact
    Klaus Kreft, Angelika Langer, Java Magazin, Juni 2010
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/51.GC.OldGen.MarkCompact/51.GC.OldGen.MarkCompact.html 
    /GC3/ 
    Garbage Collection Teil 4: Old Generation Garbage Collection - CMS
    Klaus Kreft, Angelika Langer, Java Magazin, August 2010
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/52.GC.OldGen.CMS/52.GC.OldGen.CMS.html 
    /GC4/  
    Garbage Collection Teil 8: "Garbage First" (G1) Garbage Collector
    Klaus Kreft, Angelika Langer, Java Magazin, April 2011
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/56.GC.G1.Details/56.GC.G1.Details.html 
    /JCP/ 
    Java Core Programmierung: Memory Model und Garbage Collection
    Klaus Kreft, Angelika Langer, entwickler.press, August 2011
    ISBN: 978-3-86802-075-5, E-Book-ISBN: 978-3-86802-262-9 
    /NIO2/ 
    Java 7  Teil 4: NIO2 - File System API & Asynchronous I/O
    Klaus Kreft, Angelika Langer, Java Magazin, Dezember 2011
    URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/60.Java7.NIO2/60.Java7.NIO2.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-2013 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/64.Mem.Leaks/64.Mem.Leaks.html  last update: 21 Oct 2013