document

Werbung
Plus CD!
Stellenmarkt
S. 58
Das war die JAX 2009
S. 15
Java Magazin
7.09
Java • Architekturen • SOA • Agile
www.javamagazin.de
CD-Inhalt
JavaRebel 2.0
Squish 3.4.4
WebCastellum 1.8.1
QF-Test 3.0.2
Groovy 1.6.3
Scala 2.7.4
Apache Trinidad 1.2.11
specs 1.5.0
Keynote von der
JAX 2009
Neal Ford:
Ancient Philosophers &
Blowhard
Jamborees
Alle CD-Infos
3
Mobile
JavaFX Mobile
Plattformübergreifend entwickeln
Web
Web Application Security
Gib Hackern keine Chance
48
Architektur
Lose gekoppelte Systeme
ActiveMQ und Camel
83
92
k
c
u
r
d
r
Sonde
ric
ecent
d
o
c
a
r Firm
deRückkehr
Die
der Einfachheit
62
Java Core
Einführung in Scala, Teil 4
Pattern Matching & Co
D 45 86 7
24
Google App Engine
Cloud Computing mit Java
Cloud Computing erobert die IT-Welt. Zu verlockend ist die
Möglichkeit, Hardwareressourcen nach Bedarf hinzuzufügen
und so immense Fixkosten einzusparen. Anfang April wurde
die GAE für Java als „Early Look“ veröffentlicht. Wie zeigen, wie die
Google App Engine funktioniert.
38
Sonderdruck
Auf der Jagd nach dem
verlorenen Speicher
Oft gestaltet sich die Suche nach Memory Leaks als echtes Abenteuer und man muss
sich durch einen Dschungel an Objekten und Referenzen kämpfen. Speziell wenn Produktivsysteme betroffen sind, heißt es, schnell handeln. So wie Indiana Jones immer wieder auf verblüffende Art Hinweise interpretiert und Rätsel löst, um verlorene Schätze zu
finden, wollen wir uns auf die Suche nach Memory Leaks begeben.
von Alois Reitbauer und Mirko Novakovic
m letzten Artikel haben wir bereits die drei vorkommenden
Typen von Speicherproblemen
– Memory Leaks, ineffiziente Objekterzeugung und schlechte Garbage-Collector-Konfiguration – angesprochen. Zusätzlich wurden anhand einiger Beispiele
typische Probleme aufgezeigt und erläutert. Obwohl Speicherprobleme neben
klassischen Laufzeitproblemen die
zweithäufigste Ursache für Performance­
probleme sind, bereitet deren Analyse
oft Kopfzerbrechen. In diesem Artikel
fokussieren wir daher auf die Analyse
von Speicherproblemen, indem wir einerseits methodisches Vorgehen zum
Finden der Probleme und andererseits
Details zum Speichermanagement in Java als Verständnisgrundlage erklären.
Wir packen in unseren Koffer …
… einen Heap Analyzer und eine Konsole zur Analyse von Laufzeitperfor-
2 javamagazin 7|2009
mancewerten. Mit diesen Werkzeugen
sind wir bestens für die Speicheranalyse gerüstet. Welches Werkzeug man
konkret wählt, hängt vom persönlichen
Geschmack und der eigenen Geldbörse ab. Der Open-Source-Bereich bietet
hier bereits einiges an und es mangelt
auch nicht an professionellen Werkzeugen.
Heap Dump
Ein Heap Dump ermöglicht uns, ein
Abbild des Hauptspeichers als Snapshot zu ziehen und dann zu analysieren. Man kann ihn über Funktionen
der JVM erzeugen oder über spezielle Tools, die das JVM Tool Interface
(JVMTI) nutzen. Der integrierte Heap
Dump der JVM ist leider nicht standardisiert und je nach JVM-Hersteller
unterschiedlich. Der im letzten Artikel
angesprochene JSR-326 beschäftigt
sich auch in diesem Umfeld mit einer
© Software & Support Verlag GmbH
Standardisierung. Zwar sollen hier
nicht die Datenstrukturen, sondern die
APIs standardisiert werden, was einen
einheitlichen Zugriff auf diese Daten
ermöglicht. In den meisten Fällen beinhaltet der Dump aber die Objekte auf
dem Heap inklusive der eingehenden
und ausgehenden Referenzen. Neben
diesen enthalten die Dumps oft auch
noch Informationen über die Größe
des Objekts und den Inhalt der primitiven Java-Typen. Gerade die Referenzen der Objekte untereinander sind
unerlässlich, um ein Memory Leak zu
identifizieren, denn nur wenn man die
Struktur des Speichers versteht, kann
man herausfinden, wo und wodurch
ein Speicherproblem aufgetreten ist.
Werkzeuge bieten hier idealerweise
noch die Möglichkeit, mehrere Dumps
zu vergleichen. Damit können schnell
jene Klassen gefunden werden, deren
Objekte kontinuierlich ansteigen. Zu-
www.JAXenter.de
Sonderdruck
sätzlich bieten einige Tools noch die
Möglichkeit, die Anzahl der überlebten Garbage Collections pro Objekt anzuzeigen. Dadurch kann schnell herausgefunden werden, welche Objekte
nicht freigegeben werden. Optimal ist
es, wenn man zu jedem Objekt auch den
Java Call Stack sehen kann, der dieses
Objekt erzeugt hat. Identifiziert man
erst einmal das Objekt, das ein Kandidat für ein Memory Leak ist, erhält man
so direkt die Information darüber, wo
das Objekt im Code instanziert wurde.
JVM-Metriken
Mithilfe der integrierten JVM-Metriken, die seit Java 5 standardisiert über
JMX bereitgestellt werden, können wir
den Heap zur Laufzeit beobachten. Anhand der Auslastung des Speichers können wir erkennen, ob wir überhaupt
ein Speicherproblem haben. Dieses ist
allerdings bei schleichenden Memory
Leaks, die sehr langsam entstehen, oft
über lange Zeit nur sehr schwer zu erkennen. Neben einem Überblick über
das Speicherverhalten im Groben sind
wir auch an Details zu den einzelnen
Speichergenerationen interessiert.
Auch wenn wir kein Memory Leak
haben, können wir hier sehr schön erkennen, ob unsere Anwendung eine
schlechte Konfiguration der Speicherparameter oder ineffiziente Objekterzeugung aufweist. Aus Laufzeitsicht
sind wir zudem an der Zeit, die im
Garbage Collector verbracht wird, interessiert. Hierbei interessieren uns die
Anzahl der Collections und auch deren
Zeit sowie Typ. Mehr Details dazu später. Mit diesen Werkzeugen gerüstet,
können wir uns auf die Jagd nach Speicherproblemen machen.
Ich habe da etwas vergessen
Wie schon im letzten Artikel beschrieben, handelt es sich bei Speicherproblemen in Java nicht um Leaks im klassischen Sinne. Also um Speicher, der
allokiert wurde, aber obwohl er nicht
mehr referenziert wird, noch nicht freigegeben wurde. In Java kümmert sich
die JVM um die Freigabe, wir – die Entwickler – müssen nur alle Referenzen
auf ein Objekt freigeben. Das bedeutet,
dass Speicherprobleme in Java immer
www.JAXenter.de
Abb. 1: Root-Pfad
eines HTTPSession-Objekts in
Tomcat
dann entstehen, wenn Objekte außerhalb des Scopes der aktuellen Abarbeitung referenziert werden. Die ServletSession und den Thread Local Storage
haben wir als Beispiele im letzten Artikel bereits erwähnt. Hinzu kommen
noch statische Variablen und Felder
von Objekten, die im Wesentlichen nie
aufgeräumt werden. Im Zusammenhang mit Memory-Management sind
GC Roots ein zentrales Konzept, das
man verstehen muss, wenn man die kritischen Referenzen auf ein Objekt identifizieren möchte. Garbage Collector
Roots sind Objekte, auf die es keine eingehenden Referenzen gibt und die dafür
verantwortlich sind, dass referenzierte
Objekte im Speicher gehalten werden.
Wird ein Objekt weder direkt noch indirekt von einem GC Root referenziert,
wird es als unreachable gekennzeichnet
und zur Garbage Colle���������������
ction freigegeben. Es gibt vereinfacht drei Arten von
Garbage Collection Roots:
Temporäre Variablen auf dem Stack
eines Threads
Statische Variablen von Klassen
Spezielle native Referenzen in JNI
Ein Objekt allein macht noch kein Memory Leak aus. Wenn der Speicher sich
immer weiter füllt, müssen kontinuierlich immer mehr von diesen Objekten
hinzukommen. Collections sind hier
also besonders kritisch, da diese zur
Laufzeit praktisch unbegrenzt wachsen
und dadurch auch viele Referenzen halten, die das Aufräumen von Objekten
verhindern können. Daraus können wir
ableiten, dass sich die meisten Speicherprobleme in großen Collections, die di-
rekt oder indirekt statisch referenziert
werden, manifestieren. Sehen wir uns
das am Beispiel der HTTP-Session an.
In Abbildung 1 sehen wir ganz oben die
HTTPSession oder besser gesagt deren
Implementierung in Tomcat. Die Session wird in einer ConcurrentHashmap
abgespeichert, die über den ThreadLocal des Servlet Threads referenziert
wird. Dieser ist selbst wieder in einem
Thread Array abgelegt, das Teil einer
ThreadGroup ist. Diese ThreadGroup
wird dann weiter von einem Thread referenziert.
Das zeigt, dass die meisten Speicherprobleme auf ein bestimmtes Objekt im
Heap zurückgeführt werden können –
sozusagen die Wurzel allen Übels. Des
Weiteren wird im Zusammenhang mit
der Memory-Leak-Analyse oft von so genannten Dominatoren oder Dominator
Trees gesprochen. Das Konzept der Dominatoren kommt aus der Grafentheorie
und definiert einen Knoten als Dominator eines anderen Knotens, wenn er nur
durch diesen erreicht werden kann. Auf
das Speichermangement umgesetzt, ist
also jenes Objekt Dominator eines anderen, wenn es kein zweites Objekt gibt,
das darauf Referenzen hält. Ein Dominator Tree ist dann ein Teilbaum, in dem
diese Bedingung vom Wurzelknoten aus
für alle Kinder gilt. Wird also die Wurzelreferenz freigegeben, wird auch der
ganze Dominator Tree freigegeben. Sehr
große Dominatorenbäume sind also
sehr gute potenzielle Kandidaten bei der
Memory-Leak-Suche. Abbildung 2 zeigt
einen symbolisierten Speicherbaum mit
einem Dominator-Baum. Einige Tools
verwenden die Dominator-Tree-Analyse zum Erkennen von Memory Leaks
© Software & Support Verlag GmbH
javamagazin 7|2009 3
Sonderdruck
Abb. 2:
Symbolisierter
Speichergraf mit Dominator
[2]. Andere wiederum verlassen sich
auf die simulierte Gargabe-CollectionGröße von Objekten – also die Menge an
Speicher, die bei Freigabe eines Objekts
freigegeben werden kann.
Post mortem vs. Laufzeitanalyse
Bei der Memory -Leak-Suche kann man
im Wesentlichen zwei Analyseverfahren anwenden. Das hängt auch von der
Situation ab. Wenn sich die Anwendung
bereits mit einem Out of Memory Error
verabschiedet hat, bleibt nur die Möglichkeit einer Post-mortem-Analyse.
Um diese durchzuführen, ist es allerdings
notwendig, die Sun JVM mit der Option
+HeapDumpOnOutOfMemoryError zu
starten – bei anderen JVM-Herstellern
kann der Parameter abweichen. Dann
wird automatisch versucht, einen Heap
Dump zu erstellen, bevor die JVM den
Dienst quittiert. Diese Option sollte
grundsätzlich in jeder produktiven Anwendung verwendet werden. Obwohl sie
erst mit Java 6 eingeführt wurde, haben
die JVM-Hersteller diese Option auch in
frühere JVMs rückportiert. Verwendet
man also eine ältere JVM, empfiehlt es
sich zu überprüfen, ob der verwendete
Patch-Level diese unterstützt.
Post-mortem-Dumps haben den
großen Vorteil, dass sie das Memory Leak
bereits enthalten und das Speicherproblem nicht erst aufwändig reproduziert
werden muss. Gerade bei schleichenden
Memory Leaks oder bei Problemen, die
nur in ganz speziellen Anwendungsfällen
und mit ganz besonderen Datenkonstellationen auftreten, kann es sehr schwer
sein, diese zu reproduzieren, und man
ist im Prinzip auf die Generierung des
Dumps im Fehlerfall angewiesen. Der
Nachteil ist, dass jegliche Laufzeitinformationen, also z. B. welche Methode das
Objekt erzeugt hat, verloren sind. Dafür
kann man aber meist über den Dominator-Baum sehr schnell erkennen, welche
Objekte für das Memory Leak verantwortlich sind, und mit den entsprechenden Entwicklern sollte man anhand der
Objektnamen und des Inhalts der Objekte schnell erkennen, um welche konkreten Instanzen es sich handelt. So kann
man dann die Codestellen identifizieren,
Feedback und Fragen zum Thema Performance
Im Rahmen der Performanceserie laden wir Sie ein, Fragen und Feedback an die
Autoren zu richten. Sie werden versuchen, dieses in den folgenden Artikeln einzubauen oder Antworten online zu posten, um den Inhalt auch anderen Lesern zugänglich zu machen. Erfahrungsberichte aus dem Entwickleralltag sind ebenfalls
willkommen. Kontaktieren Sie uns unter [email protected].
4 javamagazin 7|2009
© Software & Support Verlag GmbH
die die Objekte erzeugen, und entsprechend für eine Dereferenzierung an der
richtigen Stelle sorgen.
Alternativ kann es auch sein, dass
man aufgrund des zunehmenden Speicherverbrauchs schon zur Laufzeit erkennt, dass ein Memory Leak aufgetreten
ist. Natürlich ändert dies nichts daran,
dass auch hier letztendlich die JVM abstürzen wird. Allerdings können hier
schon während der Laufzeit SpeicherSnapshots gezogen werden. Da während
dieser Zeit laufende Threads angehalten
werden, empfiehlt es sich, in produktiven
Umgebungen den Server zuvor aus dem
Load Balancer eines Clusters zu entfernen. Der oder die anderen Knoten sollten
allein schon aus Ausfallgründen in der
Lage sein, die zusätzliche Last übernehmen zu können. Oft werden die gesammelten Daten für eine Analyse ausreichen. Zudem kann obiger Ansatz auch
mehrfach wiederholt werden, um so die
erzeugten Snapshots zu vergleichen und
unmittelbar zu erkennen, welche Objekte kontinuierlich anwachsen. Kombiniert man dieses Verfahren mit der
Überwachung von Col­lection-Größen,
kann man viele Memory Leaks ohne aufwendige Analysen identifizieren. Kennt
man den Inhalt der betreffenden Collections, ist dem Entwickler oft schon klar,
wo das Problem liegt. Hat man zusätzlich
noch Informationen, wo diese Objekte
angelegt wurden, ist die Ursache unmittelbar identifiziert.
Auf die Größe kommt es an
Von zentraler Bedeutung bei der Memory-Dump-Analyse ist die Größe des
Heaps. Größer bedeutet nicht besser.
Speziell 64 bit JVMs stellen hier eine
Herausforderung dar. Aufgrund der
größeren Anzahl an Objekten müssen
hier mehr Daten gedumpt werden, was
unmittelbar zu höherem Platzbedarf und
auch längerer Dump-Generierungszeit
führt. Berechnungen auf diesen Dumps
dauern natürlich ebenfalls länger. Speziell Algorithmen zur Berechnung der
Garbage-Collection-Größe sowie der
dynamischen Größe von Objekten weisen naturgemäß eine schlechtere Laufzeitperformance auf. Aus der Erfahrung
der Autoren scheitern die meisten Tools
bereits daran, größere Heap Dumps (ab 6
www.JAXenter.de
Sonderdruck
GB) überhaupt öffnen zu können. Zusätzlich ist auch noch der Laufzeit-Overhead
während der Dump-Generierung zu beachten. Die Erstellung des Heap Dumps
erfordert innerhalb der JVM Speicher.
Dies kann dazu führen, dass die Erstellung eines Dumps unter Umständen gar
nicht mehr möglich ist. Der Grund dafür
liegt in der Implementierung der HeapDump-Methoden in JVMTI. Hierbei
wird für jedes Objekt ein Tag vergeben.
Dieses Tag identifiziert anschließend
das Objekt. Um herauszufinden, welche
Objekte sich referenzieren, ist es notwendig, zuerst alle Objekte zu taggen. Dieser
Tag ist durch den JNI-Datentyp jlong
repräsentiert und nimmt pro Objekt bereits 8 Byte Speicher in Anspruch. Hinzu
kommt noch der Speicherbedarf für die
Verwaltungsstrukturen im Hintergrund.
D��������������������������������������
iese sind natürlich von der JVM-Implementierung abhängig und betragen dann
insgesamt bis zu 40 Byte pro Objekt.
Es empfiehlt sich also grundsätzlich,
mit kleineren Heaps zu arbeiten. Sie sind
leichter zu managen und im Fehlerfall
leichter zu analysieren. Speicherprobleme sind hier oft auch schneller ersichtlich. Ist eine Anwendung sehr speicherintensiv, sollte man mit mehreren
JVM-Instanzen arbeiten. Mit diesem
Ansatz erhält man zusätzliche Ausfallsicherheit fast geschenkt. Kann man – aus
welchen Gründen auch immer – nicht
auf einen großen Heap verzichten, ist
es unabdingbar, vorab zu testen, ob es
im Problemfall auch analysiert werden
kann. Sonst findet man sich schnell in
einer Situation wieder, in der man ernsthaften Speicherproblemen machtlos
– oder besser ohne Werkzeugunterstützung – gegenüber steht.
dung und Lasttreiber laufen. Mit etwas
Glück zeigen sich dann Memory Leaks als
Out of Memory Errors. Wie schon angesprochen, kommt es aber oft auf die Konstellation der Benutzertransaktionen an. In
diesem Fall kann es passieren, dass gerade
schleichende Leaks nicht unmittelbar
auftreten. Durch Vergleichen mehrerer
Heap Dumps kann man auch hier schnell
erkennen, welche Objekte kontinuierlich
anwachsen. Durch eine Heap-Analyse
lassen sich dann Programmierfehler
schneller finden.
Fazit
Memory Leaks führen in Anwendungen zwangsläufig zu Anwendungsproblemen. Anfangs kommt man sich bei
der Fehlersuche oft überfordert vor.
Gutes Verstädnis über die Struktur des
Java Heaps hilft allerdings, Probleme
schnell einzugrenzen und effektiv zu
beseitigen. Langzeittests helfen, diese
bereits während der Entwicklung zu
finden. Dennoch sind die notwendigen
Vorbereitungen, um eine effiziente Memory-Analyse durchführen zu können,
ein Kernbestandteil jedes produktiven
Anwendungs-Deployments. Einfach
den Heap zu vergrößern, stellt keine Alternative bei hohem Speicherverbrauch
dar. Das kann sogar noch zu zusätzlichen Problemen führen. Ob man eher
zur Domintor-Tree-Analyse oder Laufzeitanalyse tendiert, hängt vom persönlichen Geschmack und nicht zuletzt
von der Komplexität der Analyse ab. Für
größere Heaps ist die Laufzeitanalyse zu
bevorzugen.
Alois Reitbauer ist Technology Strategist bei dynaTrace
Software. In seiner Rolle
beeinflusst er aktiv die dynaTrace-Produktstrategie und
unterstützt Topunternehmen
bei der Einführung von PerformanceManagement-Lösungen.
Mirko Novakovic ist Mitgründer der codecentric
GmbH. Neben der Geschäftsleitung liegen seine
Schwerpunkte im Bereich
Performancetuning, JavaEE-Architekturen und Open-SourceFrameworks.
Links & Literatur
[1] Dominator and Dominator Tree, Wikipedia: http://en.wikipedia.org/wiki/
Dominator_(graph_theory )
[2] Automated Heap Dump Analysis for
Developers, Testers, and Support
Employees @ JavaOne: http://
developers.sun.com/learning/
javaoneonline/j1sessn.jsp?
sessn=TS-5729&yr=2008&
track=tools
[3] Java Virtual Machine Tool Interface:
http://java.sun.com/j2se/1.5.0/
docs/guide/jvmti/jvmti.html
[4] JSR 326 Post Mortem JVM Diagnosis API: http://jcp.org/en/jsr/
detail?id=326
[5] The Truth about Garbage Collection:
http://java.sun.com/docs/books/
performance/1st_edition/html/
JPAppGC.fm.html
[6] Java Heap Dumps erzeugen: http://
blog.codecentric.de/2008/07/
memory-analyse-teil-1-java-heapdumps-erzeugen/
Vorbeugende Memory-LeakSuche in der Entwicklung
Die besten Memory Leaks sind natürlich
jene, die man gar nicht hat. Deshalb sollte man schon während der Entwicklung
durch geeignete Tests nach potenziellen
Memory Leaks suchen. Hierfür eignen
sich langlaufende Lasttests am besten. Da
es uns in diesem Fall nicht um Performancetests, sondern um die Fehlersuche geht,
können wir mit einer k������������������
leinen Lasttestumgebung arbeiten. Oft reicht hier auch ein
einziger Rechner aus, auf dem Anwen-
www.JAXenter.de
codecentric GmbH
Ansprechpartner:
Merscheider Straße 1
42699 Solingen
www.codecentric.de
Tim van Baars
+49 (0)212 233628 - 13
[email protected]
© Software & Support Verlag GmbH
javamagazin 7|2009 5
Herunterladen