|
|||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||
|
Effective Java - Tools for Dynamic Analysis of Cyclic Memory Leaks
|
||||||||||||||||||||||||||||||||
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.
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.
Literaturverweise
Die gesamte Serie über Memory Leaks:
|
|||||||||||||||||||||||||||||||||
© 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 |