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 - Tools for Dynamic Analysis of Cyclic Memory Leaks

Effective Java - Tools for Dynamic Analysis of Cyclic Memory Leaks  
Memory Leaks
Tools für die dynamische Memory Leak Analyse
 

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

 

Nachdem wir uns in den vorangegangenen Beiträge damit befasst haben, was ein Memory Leak in Java ist und unter welchen Umständen solche Leaks auftreten, wollen wir uns in diesem und dem nächsten Beiträgen ansehen, mit welchen Strategien und Werkzeugen man nach einem vermuteten Memory Leak suchen kann.

 
Wie wir gesehen haben, gibt es unterschiedliche Arten von Memory Leaks.  Alle gehen letztlich auf eine „unwanted reference“ zurück, also eine Referenz auf ein Objekt, das von der Programmlogik her nicht mehr gebraucht wird.  Wegen der Referenz wird das fragliche Objekt vom Garbage Collector als erreichbar und benutzt erkannt und sein Speicher kann deshalb nicht freigegeben werden. 

 
 

Störend sind diese unerwünschten Referenzen, wenn sie größere Mengen an Speicher für eine längere Zeit am Leben erhalten.  Das kann im schlimmsten Falle zu einem  OutOfMemoryError der JVM und damit zum Programmabbruch führen.  Oft ist ein solcher  OutOfMemoryError der Ausgangspunkt für eine Memory-Leak-Analyse.  Allerdings ist es dann schon reichlich spät und es kann nur noch eine Post-Mortem-Analyse gemacht werden.  Dafür steht in der Regel wenig Information zur Verfügung. Oft ist es nicht mehr als ein Heap Dump.
 
 

Man muss aber nicht notwendig warten, bis eine Anwendung wegen Speichermangels abbricht, ehe man nach Memory Leaks Ausschau hält.  Man kann auch schon vorher – wenn die Applikation noch läuft - überprüfen, ob es Memory Leaks gibt.  Das ist sogar empfehlenswert, denn während des Ablaufs der Anwendung können dynamische Daten von der JVM geholt werden, die weit mehr Information für die Analyse liefern, als es ein Heap Dump tun kann.
 
 

Wir wollen uns im Folgenden beide Arten von Analyse ansehen:

- die proaktive, dynamische Analyse am „lebenden Objekt“, d.h. während die Anwendung läuft, und

- die reaktive Post-Mortem-Analyse, nachdem die Anwendung bereits (wegen Speichermangels) abgebrochen ist.
 
 

Dazu wollen wir das Beispiel aus unserem ersten Beitrag dieser Reihe wieder aufgreifen (siehe / MEMLKS-1/ ).  Dort hatten wir ein kurzes, aber fehlerhaftes Programm mit Memory Leak gezeigt.  Es ging um die Implementierung eines rudimentären Servers auf Basis der mit Java 7 eingeführten  AsynchronousSocketChannel s.  Wir hatten pro Client einen Eintrag in einer Map gemacht, den wir aber am Ende der Client-Session nicht wieder gelöscht haben.  Infolgedessen wächst die Map stetig an.  Dies führt bei unserem Server zunächst zu Speicherengpässen mit auffallend langen Stop-the-World-Pausen des Garbage Collectors und am Ende zum Absturz mit  OutOfMemoryError .  Wie findet man jetzt ein solches Memory Leak?

Dynamische Memory-Leak-Analyse mit einem Profiler

Wir wollen es gar nicht soweit kommen lassen, dass der Server mit  OutOfMemoryError abstürzt, sondern wir überlegen uns vorher, welche Daten wie lange leben sollten, und überprüfen dies.  Die zugrunde liegende Überlegung ist: nahezu jede Anwendung hat speicherneutrale Use Cases.  Während des Use Case werden Daten gebraucht, die nach dem Use Case nicht mehr referenziert und damit freigegeben sein sollten.  Wenn sie nicht freigegeben sind, haben wir ein Memory Leak, und zwar eins, das mit jeder Wiederholung des Use Cases stetig wächst.

 
 

Solche speicherneutralen Use Cases können ganz unterschiedlicher Natur sein. In unserer Server-Anwendung sind es ganz offensichtlich die einzelnen Client-Sessions.
 

 
 

Die Strategie für die dynamische Memory-Leak-Analyse ist eigentlich ganz einfach: man identifiziert Anfang und Ende des speicherneutralen Use Cases. Dann verwendet man einen Memory-Profiler, den man mit der JVM verbindet, in der die Anwendung läuft.  Am Anfang und am Ende Use Cases wird ein Memory-Snapshot gemacht und danach werden beide Snapshots verglichen.  Wenn Objekte hinzugekommen sind, die den Use Case nicht hätten überleben dürfen, dann liegt offenbar ein Memory Leak vor.   
 
 

Es sind noch ein paar Details zu beachten: man muss erst die Garbage Collection auslösen, ehe ein Snapshot gezogen wird.  Das liegt daran, dass unerreichbare Objekte nicht sofort vom Garbage Collector entfernt werden, wenn sie unerreichbar werden.  Die Speicherfreigabe passiert erst mit einiger Verzögerung, nämlich dann wenn die JVM beschließt, dass nun Zeit für eine Garbage Collection sei.  Damit in den Snapshots nur noch die tatsächlich lebenden Objekte enthalten sind, muss man die Garbage Collection explizit auslösen.  Die Profiler haben dafür einen Button, der eine Collection auslöst.
 
 

Darüber hinaus zieht man die Snapshots nicht nach einmaligem Durchlauf des Use Cases, sondern man lässt den Use Case  N-mal hintereinander durchlaufen, zieht erst danach einen Snapshot und prüft, ob unerwartete Objekte in Vielfachen von N hinzugekommen sind.  Das macht man so, weil es Referenzen geben könnte, die am Anfang des Use Case belegt werden mit der Referenz auf ein Objekt, das für den Use Case gebraucht wird.  Dann bleibt diese Referenz nach dem Zyklus stehen, bis sie zu Beginn des nächsten Durchlaufs mit dem nächsten Objekt belegt wird.  Diese Referenz würde als unerwartete Referenz am Ende des Use Cases auftauchen.  Sie ist aber kein echtes Memory Leak, denn die unerwünschte Referenz löst sich zu Beginn des nächsten Durchlaufs von alleine auf.  Solche Effekte lassen sich leichter erkennen, wenn man mehrere Durchläufe ausführt, ehe man Anfangs- und End-Snapshot miteinander vergleicht. 

 

Abb. 1: Zyklische Memory-Leak-Analyse

 

Das Problem bei dieser Art der Memory-Leak-Analyse ist, dass es mitunter nicht ganz einfach ist, Anfang und Ende eines Use Cases exakt zu bestimmen.  So auch in unserem Beispiel.  Offensichtliche speicherneutrale Use Cases in unserem Server sind die einzelnen Client-Sessions.  Eine Client-Session beginnt, wenn der Server erfolgreich einen neuen Client akzeptiert und für den Client einen Socket-Channel geöffnet hat.  Für das Akzeptieren von neuen Clients wird in NIO2 am  AsynchronousSocketChanne l ein accept-Callback eingehängt.   Unser Use Case beginnt also im accept-Callback.  Die Client-Session endet, wenn der Socket-Channel geschlossen wird, der für den Client geöffnet wurde.  Das passiert im read-Callback, den wir am Socket-Channel des Clients eingehängt haben. (Beides ist in der Box mit Source-Code 1 zu sehen.)
 
 

Da das alles asynchron in mehreren Threads abgearbeitet wird, ist es nur schwer möglich, den Anfang und das Ende einer bestimmten Client-Session zu bestimmen.  Wir wissen nur, wo irgendwelche Client-Sessions anfangen (im accept-Callback) und wo irgendwelche Client-Sessions enden (im read-Callback), aber die Zuordnung zu einem bestimmten Client ist nicht ohne weiteres möglich.  So bekommen wir mit Hilfe eines Profilers und der oben beschriebenen Strategie des Snapshot-Vergleichens nicht heraus, ob die Objekte zu einer bestimmen Client-Session am Ende der Session freigegeben wurden oder übrig geblieben sind.  Bei einer sequentiellen Abarbeitung der Client-Sessions (d.h. der Use Cases) wäre es kein Problem, aber bei Parallelverarbeitung wie in unserem Beispiel müssen wir uns etwas anderes überlegen.
 
 

In unserem Beispiel gehen wir an die Stelle, an der die Clients gestartet werden; sie werden dort als Tasks zur Ausführung an einen Thread-Pool übergeben.  Wir ändern den Code nun so, dass wir die parallele Abarbeitung der Client-Sessions künstlich sequentialisieren, um eine Memory-Leak-Analyse auf der Server-Seite machen zu können.  (Dazu muss man in der Box mit Source-Code 2 die Übergabe der Client-Tasks an den Thread-Pool per  pool.submit(new Client(serverPort)) durch einen synchronenen Aufruf der Client-Tasks per  new Client(serverPort).run() ersetzen.)
 
 

Wir wollen hier die Analyse mit dem NetBeans-Profiler machen. Das ist ein kostenfreier Profiler, der in die NetBeans-Einwicklungsumgebung eingebettet ist (siehe / NETB /).  Grundsätzlich kann man aber auch jeden anderen Profiler verwenden. 
 
 

Wir ziehen, wie oben beschrieben, einen Memory-Snapshot, nachdem eine Client-Task zur Ausführung an den Threadpool übergeben wurde – natürlich nachdem wir vor dem Snapshot die Garbage Collection ausgelöst haben.  Dann lassen wir 30 Clients loslaufen, warten eine Weile und machen danach Garbage Collection, ziehen einen weiteren Snapshot und vergleichen die beiden Snapshots.  Das Ergebnis sieht in unserem Fall so aus:
 
 


 
 

Man kann gut erkennen, dass es überlebende Objekte in Vielfachen von 30 gibt; offenbar ist pro Client-Session ein solches Objekt hinzugekommen.  Hervorzuheben ist dabei, dass es 30 Client-Session-Objekte vom Typ  test.AbstractServer$ClientSession gibt.  Das wäre nicht weiter bemerkenswert, wenn es dazu aktive Clients gäbe, zu diesen Client-Session-Objekte korrespondieren würden.  Aber Client-Objekte vom Typ  test.Client gibt es keine.  Das ist also höchst verdächtig!
 
 

Nun kann man diesen Snapshot-Vergleich einige Male wiederholen.  Dabei wird sich bestätigen, dass immer mehr Client-Session-Objekte übrig bleiben.  Als nächstes wird man versuchen heraus zu finden, wer die Client-Session-Objekte erzeugt hat und wer sie am Leben erhält.  Der Erzeuger ist leicht gefunden, denn alle Profiler können im Allgemeinen Allocation-Information bereitstellen und zu einem Objekttyp die allozierenden Methoden anzeigen.  Damit ist schnell gefunden, dass die Client-Session-Objekte am Anfang der Session im accept.-Callback erzeugt werden.  Das ist nicht sonderlich überraschend und erklärt leider nicht, warum sich die Client-Session-Objekte anhäufen.
 
 

Interessanter ist, wer die Client-Session-Objekte referenziert hält.  Hier unterscheiden sich die Profiler in ihren Features recht heftig.  Manche Profiler (z.B. JProbe) erlauben es, genau für die pro Zyklus hinzugekommenen Objekte die ein- und ausgehenden Referenzen zu verfolgen.   Der NetBeans-Profiler hat dafür keine Unterstützung; er hat keine Navigationsmöglichkeiten von den hinzugekommenen Objekten, die man in der Snapshot-Differenz sieht, hin zu den Referenzen auf diese Objekte.  Man kann sich hier aber behelfen, indem man am Ende eines Zyklus einen Heap Dump zieht und sich darin anschaut, welche Referenzen es auf Client-Session-Objekte gibt.
 
 


 
 

Man findet im Heap Dump rasch, dass die Client-Session-Objekte von einer  ConcurrentHashMap namens  segments im Server referenziert werden.  Warum sie von dort auch nach dem Ende einer Client-Session noch referenziert werden, kann der Profiler allerdings nicht erklären.  Dazu muss der Entwickler den Source-Code der Implementierung analysieren und die Programmlogik verstehen.   Der Profiler kann nur Hinweise geben und Hilfestellungen bei der Diagnose leisten.  Die Ursache des Problems zu verstehen und das Problem dann auch zu beheben, ist und bleibt Sache des Entwicklers. Das ist so ähnlich wie bei Debuggen: der Debugger findet die Fehler nicht, er kann nur bei der Fehlersuche helfen.
 
 

Das Ergebnis unserer dynamischen Memory-Leak-Analyse ist also: in einer  ConcurrentHashMap im Server akkumulieren sich Client-Session-Daten.  Warum sie das tun, muss man sich aus der Programmlogik herleiten.
 
 

Kann man zu diesem Ergebnis auch noch kommen, wenn der Server bereits wegen Speichermangel abgestürzt ist und nur noch einen Heap Dump hinterlassen hat?  Dazu muss man den Heap-Dump mit einem entsprechenden Analyzer untersuchen.  Das sehen wir uns im nächsten Beitrag an.

Automatische Memory-Leak-Analyse mit einem Application Monitor

Wer sich die Arbeit der manuellen Suche mit Hilfe speicherneutraler Use Cases nach anwachsenden Memory Leaks nicht machen will, kann es auch mit einem Application-Monitor versuchen.  Solche Monitore verbinden sich mit einer laufenden Anwendung, beobachten sie und versuchen, nach gewissen Heuristiken mögliche Memory Leaks zu identifizieren.  Ein Beispiel für eine solche automatische Memory-Leak-Suche liefert das freie Werkzeug Antorcha Memory Plumber / PLUM /.  Es beobachtet die Größe der Collections auf dem Heap und sucht nach solchen Collections, die kontinuierlich anwachsen. Als Ergebnis wird eine Liste von verdächtigen Collections geliefert zusammen mit dem Stack-Trace der Methoden, die Elemente in die verdächtigen Collections einfügen und für das Anwachsen verantwortlich sind.

 
 

Zusammenfassung

In diesem Artikel haben wir  eine Strategie zur Memory-Leak-Analyse vorgestellt: man kann proaktiv eine zyklische Analyse machen und nach Memory Leaks suchen, ehe sie zu einem  OutOfMemoryError der JVM führen. Wir haben für die Analyse ein kostenloses, frei verfügbares Werkzeug, den NetBeans-Profiler, verwendet. Alternativ kann man es auch mit einem kostenpflichtigen Profiler wie YourKit, JProfiler oder JProbe machen. Wir haben ergänzend auf Application-Monitore hingewiesen, die nach kontinuierlich anwachsenden Collections suchen und diese als mögliche Memory-Leak-Verdächtige melden.  In allen Fällen wird man feststellen, dass die Werkzeuge nur Hilfestellungen bei der Memory Leak Analyse leisten können.  Die wirkliche Ursache muss der Entwickler selber finden; nur er versteht die Programmlogik. 
 
 
 
Source-Code 1: Auszug aus einer Server-Implementierung mit Memory Leak
public class Server extends AbstractServer {
    private final Map<ClientSession, AsynchronousSocketChannel> sessionToChannel 
                = new ConcurrentHashMap<ClientSession, AsynchronousSocketChannel>();
    public Server(int serverPort) throws IOException {
        super(serverPort);
    }

    public void doAccepting() {

        serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel,Object>() {

            @Override

            public void completed(AsynchronousSocketChannel channel, Object attachment) {

                ClientSession session = new ClientSession();

                sessionToChannel.put(session, channel);                     //    <= Anfang einer Client-Session

                final ByteBuffer buf = ByteBuffer.allocateDirect(256);

                channel.read(buf, session, new CompletionHandler<Integer, ClientSession>() {

                    @Override

                    public void completed(Integer len, ClientSession clSession) {

                        AsynchronousSocketChannel channel = sessionToChannel.get(clSession);

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

                            buf.clear();

                            channel.read(buf, clSession, this);

                        }

                        else {

                            try {

                                channel.close();                            //    <= Ende einer Client-Session

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

                        }

                    }

                    …

                });

                serverSocketChannel.accept(null, this);

             }

            

        });

    } 

}


 
 
 
Source-Code 2:  Der Test-Treiber
public final class Test {
    private static final int serverPort = 666;
    public static void main(String[] args) {
        AbstractServer theServer = null;
        try {
            theServer = new Server(serverPort);
        } catch (IOException e) {

            System.out.println("exception starting server: " + e);

            e.printStackTrace();

            System.exit(-1);

        }

        System.out.println("server started ... " + theServer);

        theServer.doAccepting();

        ExecutorService pool = new ThreadPoolExecutor(8,8,0,TimeUnit.MILLISECONDS,

                                new ArrayBlockingQueue<Runnable>(64),new ThreadPoolExecutor.CallerRunsPolicy());

        byte[] buffer = new byte[256];

        for (int i=0; i<1024; i++) {

            try {

                pool.submit(new Client(serverPort));          //    <=  parallele Abarbeitung der Client-Sessions

            } catch (IOException e) {

                System.out.println("exception creating client: " + e);

                e.printStackTrace();

            }     

        }

        pool.shutdown();

    }

}

Literaturverweise

/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
/NETB/
NetBeans Profiler
/PLUM/              
Antorcha Memory Plumber

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/67.MemLeak.ToolCyclic/67.MemLeak.ToolCyclic.html  last update: 29 Nov 2016