Blog

Java Heap-Dump-Analyse

Jeder von Euch kennt sicherlich die Situation, dass Java Anwendungsserver sehr viel Speicher benötigen und dort im schlechtesten Fall sogar OutOfMemory-Exceptions auftreten können. Die Ursachen für die hohen Speicheranforderungen können dabei sehr vielfältig sein: sie reichen von konstant wachsenden Systemen über speicherhungrige Anwendungen, die große Mengen an Daten zwischenspeichern, bis zu Fehlerverhalten der Anwendungsserver.

Oftmals wird bei Speicherproblemen leider oft noch der „einfachste“ Weg gewählt, indem dem Anwendungsserver mehr Speicher zugeteilt und nicht weiter nach den eigentlichen Ursachen des hohen Speicherverbrauchs geforscht wird. Dabei ist bei diesem Vorgehen in der Regel unklar, ob damit das ursächliche Problem behoben oder das erneute Auftreten des Problems einfach nur zeitlich herausgezögert wird.

Ein probates Mittel, solche Speicherprobleme zu analysieren und zu beheben, sind dabei Heap-Dumps. Bei einem Heap-Dump handelt es sich dabei um ein zu einem definierten Zeitpunkt erstelltes Speicherabbild der JVM eines Java Programms. Seit der Java-Version 1.6 existiert im JDK ein kleines Tool, das die Erstellung von Heap-Dumps zur Laufzeit des Anwendungsservers ermöglicht.

Erstellung eines Heap-Dumps

Seit dieser JDK-Version können über den jmap-Befehl zur Laufzeit einer JVM Heap-Dumps generiert werden:

jmap -dump:live,format=b,file=<Dateiname> <PID>

Die PID muss dafür vorher über das Betriebssystem oder über den ebenfalls aus dem JDK stammenden jps Befehl ermittelt werden.
Neben der Erstellung eines Heap-Dumps können durch den jmap-Befehl noch weitere recht hilfreiche Daten abgefragt werden.
Dies beinhaltet unter anderem eine Übersicht über die Auslastung und die Aufteilung des allokierten Speichers über die unterschiedlichen Speicherbereiche der JVM, ein Histogramm des allokierten Speichers auf Klassenebene sowie eine detailierte Übersicht über den PermGenSpace der JVM, der übrigens nicht in Heap-Dumps enthalten ist.

# Abfrage der PID
jps
ps auxw | grep java

# Erstellung eines Heap-Dumps zur Laufzeit
jmap -dump:live,format=b,file=<Dateiname> <PID>

# Abfrage der derzeitgen Speicherbelegung der JVM
jmap -heap <PID>

# Abfrage der Speicherverteilung auf Klassen (:live == gibt nur lebendige Instanzen aus)
jmap -histo:live <PID>

# Abfrage von Informationen bezüglich der PermGenSpace der JVM
jmap -permstat &lt;PID&gt;

Auswertung des erstellten Heap-Dumps

Nachdem der Heap-Dump erstellt wurde, kann er über viele Tools eingesehen und ausgewertet werden.
Das JDK 1.6 bringt ein kleines Tool mit Namen jhat zur Analyse von Heap-Dumps mit.

# Allgemeiner Aufruf
jhat <dumpfile>

# mit mehr Speicher aufrufen
jhat -J-mx2000m <dumpfile>

Jhat startet dabei einen lokalen Webserver – der Dump kann dann unter Verwendung der Defaulteinstellungen über http://localhost:7000 analysiert werden.
Das Tool stellt einem dabei aber leider nur sehr rudimentäre Funktionen zur Auswertung des Heap-Dumps zur Verfügung, außerdem ist es nicht sehr gut zu bedienen.

Eclipse Memory Analyzer

Beim Eclipse Memory Analyzer (MAT) handelt es sich um ein weitaus besser geeignetes Tool. Es bietet neben geringen Systemanforderungen eine automatische Analysefunktion, die sehr hilfreich bei der Suche nach Ursachen für einen hohen Speicherverbrauch sein kann. Außerdem ist das Tool dabei sehr einfach zu bedienen und bietet dem Benutzer dabei die Möglichkeit, sehr komfortabel durch den allokierten Speicher zu navigieren. Zusätzlich können Heap-Dumps von lokalen JVMs komfortabel aus dem Programm heraus erstellt werden.

VisualVM

Ein weiteres erwähnenswertes Tool ist VisualVM. Dabei handelt es sich nicht nur um ein Tool zur Auswertung von Heap-Dumps, sondern es kann zusätzlich auch zur Einsicht und Auswertung verschiedener Metriken von lokalen und verteilten JVMs verwendet werden, die das Tool direkt zur Laufzeit über JMX (per RMI bei Remote Systemen) bei der überwachten JVM abfragt.

Einige dieser Metriken sind beispielsweise:

  • Speichergrößen und Auslastung/Verteilung der unterschiedlichen Bereiche
  • CPU-Auslastung
  • Threads
  • Garbage Collector-Verhalten
  • Heap-Dumps können auf Knopfdruck erstellt werden

Damit VisualVM per RMI auf ein Remote-System zugreifen kann, muss auf dem zu überwachenden System jstatd ausgeführt werden.
Zur Einrichtung von jstatd müssen folgende Schritte durchgeführt werden:

1. Zunächt muss ins JAVA_HOME/bin/ oder in ein beliebiges Verzeichnis eine Datei jstatd.all.policy mit folgendem Inhalt angelegt werden

grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;};

2. Anschließend kann jstat mit folgenden Parametern ausgeführt werden.

./jstatd -J-Djava.security.policy=jstatd.all.policy -p <PORT>

Der Port kann über den „-p“ Parameter frei gewählt werden, muss dann aber noch in der Firewall des Remote-Systems freigeschaltet werden.
Eine andere Möglichkeit ist es, einen SSH-Tunnel zum Zugriff auf das Remote-System zu verwenden.

In Produktionsumgebungen werden beide genannte Varianten nicht unbedingt auf Gegenliebe bei Systemadministratoren stoßen.
Deswegen ist dort die Erstellung von Heap-Dumps über jmap anstelle der dynamischen Erstellung über VisualVM vorzuziehen.

Welches Tool sollte man nun nehmen?

Man kann beide Tools durchweg empfehlen.

VisualVM kann sehr gut verwendet werden, wenn die zu überwachende JVM lokal läuft und neben den Heap-Dumps noch andere Metriken überwacht werden sollen.
Falls eine Remote-JVM überwacht werden soll, sollte geklärt werden, ob überhaupt ein Zugriff auf das zu überwachende System möglich und auch erlaubt ist.

MAT sticht wiederum mit einer sehr guten und einfachen Bedienbarkeit und durch die automatisierten Reports hervor.

Praxisbeispiele

Finden der Ursache von OutOfMemoryExceptions nach Infrastrukturproblemen

Vor kurzem hatten wir ein Problem mit einem JBoss 5.1-Anwendungsserver. Bei dem System traten damals sehr oft OutOfMemory Exceptions auf, nachdem ein Webservice eines externes Systems aufgrund von Infrastrukturproblemen für kurze Zeit nicht mehr angesprochen werden konnte. Wir haben daraufhin in unserer Entwicklungsumgebung das Problem simuliert und dabei zur Laufzeit Heap-Dumps erzeugt, die wir dann mit dem MAT ausgewertet haben. Das Tool lieferte dabei den folgenden Hinweis:

One instance of
   "org.jboss.ejb3.stateless.StatelessContainer"
loaded by
   "org.jboss.classloader.spi.base.BaseClassLoader @ 0xae80fbd8"
occupies 598.871.696 (64,36%) bytes.
The memory is accumulated in one instance of
   "java.util.LinkedList$Entry"
loaded by
   "<system class loader>".

Über die Navigationsfunktion des MAT stellten wir dann fest, dass die Standardeinstellungen des JBoss aktiv waren und ein ThreadlocalPool für die EJBs verwendet wurde. Durch eine Internetrecherche fanden wir anschließend heraus, dass der JBoss-Anwendungsserver in der verwendeten Version beim Auftreten von nicht abgefangenen Exceptions den entsprechenden Thread aus dem Threadpool des JBoss entfernt, nicht aber dessen threadlokale EJB Pool und dessen EJB Instanzen zerstört wird.
Der Fehler konnte daraufhin durch einen Wechsel des EJB-Pooltyps auf den StrictMaxPool behoben werden.

Verbesserung der Anwendungen:

Bei der Analyse des oben genannten Fehlers ist uns zusätzlich aufgefallen, dass jede EJB-Instanz sehr viel Speicher allokierte.
Der hohe Speicherverbrauch wurde zum großen Teil durch die von uns verwendeten Webserviceclients verursacht. Jede EJB Instanz besaß dabei ihre eigene Websservice Client Instanz. Daraufhin wurden die Webservice-Clients so optimiert, so dass ein großer Teil der vom Webservice-Client allokierten Resourcen statisch und nicht mehr pro Client Instanz gebunden werden. Der Speicherbedarf konnte daraufhin drastisch reduziert werden.

Diese beiden Beispiele zeigen, dass die Heap-Dump-Analyse ein sehr einfaches und gutes Mittel ist, den Speicherbedarf und damit auch die Performance eines Systems zu verbessern.

Weitere Maßnahmen

Ein Nachteil der Heap-Dumps ist, dass es sich nur um eine Momentaufnahme handelt. Durch diesen Dump können keine Aussagen über die Entwicklung der Speicherauslastung getroffen werden.

Deshalb sollte parallel zu jeder Heap-Dump-Analyse auch immer das Garbage Collector-Verhalten überprüft werden. Dafür können die GCs über längere Zeit geloggt und damit die langfristige Entwicklung des Speichers überwacht werden. Das GC-Logging kann auf einfache Weise beim Starten jedes Anwendungsservers über JVM-Parameter eingestellt werden.

# Aktiviert das Logging der GC Aktivitäten.
-XX:+PrintGC
# Aktiviert ein detailiertes Logging der GC Aktivitäten
-XX:+PrintGCDetails
# Setzt lesbare Timestamps.
-XX:+PrintGCDateStamps
# Angabe des Logfiles.
-Xloggc:<logfile>
# Logfile Rotation - Seit Java 6 Update 34
-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=10 
-XX:GCLogFileSize=10M

Über den Autor

2 Kommentare

Antwort hinterlassen