|
|||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||
|
Effective Java - Post Mortem Heap Dump Analysis
|
||||||||||||||||||||||||||||||
Im
vorangegangenen Beitrag unserer Reihe über Memory Leaks in Java haben
wir uns angesehen, wie man mit Hilfe von Profilern nach Memory Leaks suchen
kann. Dieses Mal wollen wir diskutieren, wie man in Heap Dumps Memory
Leaks aufspüren kann.
Im vorangegangenen Beitrag haben wir eine
dynamische Memory-Leak-Analyse betrachtet, bei der wir uns mit einem Profiler
an eine laufende Anwendung gehängt, speicherneutrale Use Cases beobachtet
und nach Objekten gesucht haben, die die Use Cases überleben, obwohl sie
eigentlich nur für die Verarbeitung gebraucht wurden und hinterher hätten
verschwunden sein sollten.
Dieses Mal wollen wir uns die Post-Mortem-Analyse
ansehen und wie man sie durchführt, nachdem die Anwendung sich bereits
(wegen Speichermangels) beendet hat. Für diese Analyse steht in der Regel
wenig Information zur Verfügung. Oft ist es nur der Heap Dump, den die
JVM beim Abbruch erzeugt hat.
Wie beim letzten Artikel wollen wir wieder
das Beispiel aus unserem ersten Artikel der Reihe über Memory Leaks aufgreifen
(siehe /
MEMLKS-1
/). Dort hatten wir ein kurzes,
aber fehlerhaftes Programm mit Memory Leak angesehen. 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 nun ein solches Memory Leak, wenn man nichts weiter als
einen Heap Dump zur Verfügung hat?
Post-Mortem-Memory-Leak-Analyse
Nehmen wir also an, unser Server ist mit
der JVM-Option
-XX:+HeapDumpOnOutOfMemoryError
gestartet worden; dann wird der Heap Dump im Falle der JVM-Terminierung
wegen
OutOfMemoryError
automatisch erzeugt.
Man kann Heap Dumps auch anders erzeugen; siehe dazu die Box „Wie bekomme
ich einen Heap Dump?“
Ein gutes Werkzeug für die Analyse von Heap
Dumps ist der kostenlose Memory Analyzer MAT (siehe /
MAT
/).
Er hat gegenüber anderen Werkzeugen den Vorteil, dass er auch sehr große
Heap Dumps verarbeiten kann. Das schafft er, weil er den Heap Dump
zuallererst einmal indiziert, damit er hinterher in vertretbarer Zeit und
mit verkraftbarem Speicherverbrauch eine Analyse auf dem Dump ermöglichen
kann.
Dominatoren
MAT analysiert die Referenzbeziehungen
zwischen den Objekten auf dem Heap und erzeugt daraus einen sogenannten
Dominator-Baum (engl.
dominator tree
). Im Dominator-Baum sind
die Verweise zwischen den Objekten so arrangiert, dass die Verweise von
einem dominierenden Knoten zu den dominierten Knoten verlaufen. Dabei
ist ein dominierender Knoten, d.h. der Dominator, derjenige Knoten, den
man im „normalen“
Referenzgrafen passieren muss, um die dominierten
Knoten zu erreichen. Das kann man schön am Beispiel einer doppelt-verketteten
Liste sehen.
Die „normalen“ Referenzbeziehungen
sind relativ kompliziert, denn von jedem Knoten in der Liste gehen drei
weitere Verweise aus: vorwärts zum nächsten Knoten, rückwärts zum vorangegangen
Knoten und ein Verweis auf die Nutzdaten im Knoten. Meistens sind die
Knoten einer doppelt-verketteten Liste auch noch zyklisch verknüpft (d.h.
der letzte Knoten verweist wieder auf den ersten), so dass es beim Betrachten
der einzelnen Listenknoten und ihrer Verweisbeziehungen nicht ohne weiteres
zu erkennen ist, wer wen am Leben erhält.
Im Dominator-Baum sind die Knoten anders
arrangiert. Um von dem Objekt, das auf die Liste verweist (Knoten ganz
oben) zu einem der Listenelemente zu gelangen, muss man stets den ersten
Knoten in der Liste passieren. Dieser Eintrittsknoten dominiert deshalb
alle anderen Listenknoten und steht im Dominator-Baum über den anderen
Listenknoten. Wenn man nun für jeden Knoten im Dominator-Baum berechnet,
wie viel Speicher an dem Knoten dranhängt, d.h. an allen Referenzen, die
von diesem Knoten ausgehen, dann sieht man sehr gut die Akkumulierungspunkte.
Im obigen Beispiel ist es die eine Referenz vom obersten Objekt, die auf
den Eintrittsknoten der Liste zeigt, die die gesamte Liste und alle ihre
Elemente am Leben erhält.
Der Memory Analyzer MAT baut aus den Referenzen,
die er im Heap Dump findet, einen solchen Dominator-Baum für alle Java
Objekte auf, sucht die Akkumulierungspunkte heraus und bietet diese als
„leak suspects“ für die Analyse an. Mit anderen Worten, MAT sucht
aus den Objekten auf dem Heap diejenigen heraus, die große Mengen Speicher
am Leben erhalten. Für unser Beispiel sieht der Suspect Report so aus:
In unserem Beispiel wird ein HashMap-Segment
als Hauptverdächtiger präsentiert. In der Tat, an dem verdächtigen
Objekt hängt knapp die Hälfte des verbrauchten Speichers der Anwendung.
Man schaut sich dann an, was die verdächtige Map enthält und wer die
Map referenziert. Auch diese Information ist im MAT gut aufbereitet:
Man sieht sofort, dass es sich bei der verdächtigen
Map um die
ConcurrentHashMap
namens
segments
im Server handelt, die wir auch mittels der dynamischen Analyse im letzten
Artikel gefunden hatten.
Noch ein paar
Tipps zu Memory-Leak-
Analyse
In der Realität ist es u.U. etwas schwieriger,
das Memory Leak zu identifizieren. Beispielweise findet man das Leak
nicht so leicht, wenn man die Heap Dumps schon relativ früh zieht, d.h.
nicht erst beim
OutOfMemoryError
. Dann
kann es vorkommen, dass die Map noch vergleichsweise klein ist und nicht
verdächtiger Dominator identifiziert wird. In unserem Beispiel war die
Finalizer-Queue anfangs deutlich größer war als unsere Map, die das eigentliche
Problem darstellt. Man muss dem Leak also Zeit geben, als Dominator sichtbar
zu werden.
Im Übrigen sei noch einmal darauf hingewiesen,
dass nicht jedes Programm, das wegen einem
OutOfMemoryError
abstürzt, automatisch immer ein Memory Leak haben muss. Manche Anwendungen
brauchen einfach mehr Speicher, als ihnen zur Verfügung gestellt wurde.
Ob der Absturz durch ein Memory Leak verursacht wurde, kann man herausfinden,
indem man der Anwendung mehr Speicher mit
–Xmx<size>
zuteilt. Wenn sie dann immer noch abstürzt, schaut man sich die Dominatoren
in den OutOfMemory-Heap-Dumps an. Ein Dominator, der bei mehr Speicher
noch größer ist als vorher bei weniger Speicher, ist unter Umständen
ein Hinweis auf ein Memory Leak.
Ansonsten hilft es, wenn man den Text liest,
der im
OutOfMemoryError
enthalten ist.
Nicht immer heißt es:
java.lang.OutOfMemoryError:
„
Java
heap space
“. Texte wie „
requested array
size exceeds VM limit
“, „
PermGen space
”
oder “
<reason> <stack trace> (Native method).
”
deuten auf andere Ursachen hin, z.B. Fragmentierung, Perm-Bereich zu klein,
Probleme in JNI-Teilen der Anwendung, oder ähnliches.
Zusammenfassung
In diesem Artikel haben wir
die
Suche nach Memory Leaks mit Hilfe eines Heap Dumps betrachtet. Eine solche
Analyse des Heap Dumps wird oft post mortem gemacht, nachdem die Anwendung
bereits wegen einem
OutOfMemoryError
abgebrochen wurde. Wir haben für die Heap-Dump-Analyse das frei verfügbare
Werkzeug Memory Analyzer MAT verwendet, das auch sehr große Heap Dump
problemlos verarbeiten kann. Alternativ kann man es auch mit einem kostenpflichtigen
Profiler wie YourKit, JProfiler oder JProbe machen. In allen Fällen
wird man aber 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/68.MemLeak.ToolDump/68.MemLeak.ToolDump.html> last update: 29 Nov 2016 |