document

Werbung
Plus CD!
Stellenmarkt
S. 62
Das Tutorial: Lose Kopplung mit Mule
S. 71
Java Magazin
8.09
Java • Architekturen • SOA • Agile
www.javamagazin.de
ALLE IN
CD-Inhalt
FOS ZU
AB SEIT
Apache Commons
Compress
R
E 29
Grails 1.1.1
Spring dm 1.2
Project Mojarra
Scala 2.7.5
Mule 2.2.1
Die nächste Generation der
Webentwicklung
42
Ed Burns:
Secrets of
the Rockstar
Programmers
Session von der
JAX 2009
Alle CD-Infos
3
Java Core
OSGi on Scala
DSLs mit ScalaModules
18
XML vs. Annotations
t
ecen
d
o
c
a
der Firm
Sind Annotations die
erhoffte Rettung? 37
Tools
Java-Spracherweiterung
mit JetBrains MPS
Effizient DSLs erstellen
Quo vadis JSF?
Interview mit Ed Burns
52
78
Grails in Action
Enterprise
Garbage Collector
Das unbekannte Wesen
k
c
u
r
d
r
Sonde
ric
65
Erfahrungsbericht: WerKannWann.de
56
D 45 86 7
OSGi mit Spring dm
Viele Köche verderben (nicht) den Brei
24
Sonderdruck
Der Garbage Collector –
das unbekannte Wesen?
In den letzten beiden Artikeln haben wir uns bereits mit dem Thema Speichermanagement in Java beschäftigt. Nach der Vorstellung allgemeiner Antipatterns wurden
die Entstehung und das Auffinden von Memoryleaks genauer beleuchtet. In diesem
Artikel werden wir uns detaillierter mit dem Thema Garbage Collection befassen.
von Alois Reitbauer und Mirko Novakovic
ie falsche (oder fehlende) Konfiguration des Garbage Collectors
führt häufig zu Performanceproblemen. Sehr oft werden schlechte
Antwortzeiten allerdings nicht sofort mit
einem schlecht konfigurierten Garbage
Collector in Verbindung gebracht, weil es
schwierig ist, die GC-Zeiten mit Antwortzeiten oder sogar Methodenlaufzeiten
zu korrelieren. Ein weiteres Problem bei
der Verwendung des Garbage Collectors
ist die deklarative Konfiguration. Es wird
nicht konfiguriert, wann Objekte aus dem
Speicher entfernt werden, sondern nur
mit welcher Strategie. Ein gutes Verständis von Garbage-Collector-Algorithmen
ist hier also eine Grundvoraussetzung für
performante Anwendungen.
Die Zutaten
Im Prinzip benötigt man nur ein paar wenige Zutaten, um die Garbage-CollectorAlgorithmen zu verstehen und optimie-
Abb. 1: GC Overhead bei Multiprozessormaschinen
2 javamagazin 8|2009
ren zu können: die Basisalgorithmen von
Garbage Collectoren und ein Verständnis
für Pausenzeiten, Parallelität und Nebenläufigkeit. Die ersten JVM-Implementierungen hatten einen sehr einfachen Markand-Sweep-Algorithmus verwendet, um
den Speicher von Müll zu befreien. Das
Prinzip ist einfach. Alle aktiven Threads
werden gestoppt und beginnend bei den
GC Roots alle Objekte markiert, die von
einem GC Root aus noch erreichbar sind,
sprich, referenziert werden. Die restlichen
Objekte sind Müll und werden vom Garbage Collector beseitigt. Dabei entstehen
sehr schnell zwei Probleme: Erstens stört
das Anhalten aller Threads (also die Pausenzeiten) und auf Dauer wird der Speicher fragmentiert und es kann zu OutOfMemory-Problemen kommen, obwohl
noch genug Speicher vorhanden ist. Das
passiert dann, wenn nicht mehr genug
Speicher am Stück vorhanden ist, um ein
Objekt im Heap abzulegen. Schnell wurden hierfür Lösungen entwickelt. Compacting Garbage Collectoren lösen das
Problem mit der Fragmentierung – sie
kopieren die Objekte regelmäßig vom
Beginn des Heaps nacheinander, sodass
keine Lücken mehr vorhanden sind und
wieder der maximal verfügbare Speicher am Stück verwendet werden kann.
Natürlich dauert das auch seine Zeit und
die Pausen werden noch länger. Gerade
bei Multiprozessormaschinen ist das extrem störend, da alle Threads angehalten
werden und nur ein Thread die Garbage
Collection ausführt. Der bekannte Graf
© Software & Support Verlag GmbH
aus der Sun-Dokumentation verdeutlicht
dieses Problem (Abb. 1).
Ein Garbage Collector, der 1% Overhead bei einer CPU verursacht, löst mehr
als 20% Overhead bei 32 CPUs aus, da
diese nicht arbeiten können, während der
Garbage Collector seine Arbeit verrichtet. Natürlich wird schnell klar, wie man
das Problem mit den Pausenzeiten lösen
könnte: Man müsste einfach den GarbageCollector-Algorithmus parallelisieren.
Bei Multiprozessormaschinen verrichtet
dann nicht mehr ein Thread die Arbeit,
sondern es werden mehrere GC-Threads
parallel eingesetzt, um den Müll schneller
zu beseitigen. Zudem wurden nebenläufige Garbage Collectoren entwickelt, die parallel zur „normalen“ Arbeit den Speicher
analysierten und Müll vormerkten, sodass
die Pausenzeiten reduziert werden. Compacting-Phasen und das Ermitteln der GC
Roots können allerdings nicht nebenläufig passieren. Die Kombination aus Markand-Sweep, Compacting, Parallelität und
Nebenläufigkeit ist bis heute im Einsatz
und die Basis der meisten angebotenen
Garbage Collectoren. Zusätzlich wurde
aber noch der Copying Garbage Collector
entwickelt. Dieser räumt den Müll nicht
weg, sondern kopiert die lebenden Objekte in einen anderen Bereich des Heaps.
Dazu teilt er den Heap in zwei gleich große
Bereiche ein. Während einer GC-Phase
werden die lebenden Objekte von einem
Bereich an den Anfang des anderen Bereichs kopiert. Der erste Bereich gilt dann
als bereinigt. Fragmentation tritt nicht auf.
www.JAXenter.de
Sonderdruck
Diese Form des Garbage Collectors eignet
sich vor allem dann sehr gut, wenn es sehr
viel Müll zu beseitigen gibt.
Viele Köche ...
Sehr oft wird bei der Erklärung der Garbage Collection so getan, als gäbe es nur eine
JVM (nämlich die Sun JVM) beziehungsweise als würden die Implementierungen
der verschiedenen Hersteller identisch
sein. Dem ist allerdings nicht so. Wir wollen uns deshalb im ersten Schritt die am
weitesten verbreiteten JVMs ansehen.
Die wohl bekannteste ist die Sun JVM. Sie
verfügt seit der Version 1.4 über die Java
HotSpot VM [1] und eine Generational
Garbage Collection. Der Heap wird hierbei in mehrere Bereiche aufgeteilt.
Abbildung 2 zeigt die zwei Hauptbereiche des Heaps, die Young und die
Tenured Generation (oft Old Generation
genannt). Die Young Generation selbst
teilt sich in den Eden Space und die Survivor Spaces auf. Objekte werden im Eden
Space erzeugt und im Laufe von Garbage
Collections dann in den Survivor und
später die Tenured Generation verlagert,
sofern noch Referenzen darauf existieren.
Klassen werden bei Sun in die so genannte
Permanent Generation geladen und liegen
somit nicht auf dem „normalen“ Heap.
Die IBM JVM hat sehr lange am klassischen Marc-and-Sweep-Algorithmus
festgehalten, bietet seit Java 5 aber sowohl
die Möglichkeit, einen kontinuierlichen
Heap oder ebenfalls einen Generational
Heap zu verwenden. Der kontinuierliche
Heap wird bei Anwendungen mit kleinen
Heap (100 MB) empfohlen. Das Layout
des Generational Heap von IBM ist ebenfalls in der Abbildung zu sehen. Das Nursery ist das Equivalent zur Young Generation des Sun Heaps und das Tenured Space
ist equivalent zum Tenured Space der Sun
JVM. Das Nursery Space teilt sich hier in
zwei gleich große Teile auf, den Allocate
Space und den Survivor Space. Sie werden
bei einer Garbage Collection geswitcht.
Die Oracle JRockit JVM verfügt wie
die IBM JVM über die Möglichkeit, einen
kontinuierlichen Heap oder einen Generational Heap zu verwenden. Auch bei
Oracle wird zwischen einer Young Space
und Old Space unterschieden. Im Gegensatz zu den anderen Implementierungen
besitzt die JRockit JVM keinen Survior
www.JAXenter.de
Space. Stattdessen verfügt sie über eine
so genannte Keep Area, in der die zuletzt
allokierten Objekte enthalten sind. Diese
werden im Fall einer Garbage Collection
nicht in die Old Generation verschoben.
Abb. 2:
Aufbau
der JVM
Heaps
Generation Y(oung)
Allen JVMs ist also gemein, dass sie über
einen Generational Heap verfügen. Die
Motivation dafür ist die Tatsache, dass
die meisten Objekte über eine kurze Lebenszeit verfügen. Für diese kurzlebigen
Objekte wird also die Garbage Collection
optimiert, was durch effizientes Freigeben
nicht mehr benötigter Objekte erreicht
wird. In den meisten Fällen kommt deshalb eine Variante des Copying Collectors
zum Einsatz. Bei der Sun JVM werden in
der Young Generation Objekte ständig
zwischen Survivor Space I und Survivor
Space II hin und her, beziehungsweise aus
dem Eden Space kopiert (Minor Collection) , in der Hoffnung, dass die Objekte
die nächste Aufräumaktion nicht überleben. Zu jedem Objekt wird erfasst, wie
oft es bereits hin und her kopiert wurde
– ist ein konfiguriertes Limit erreicht, gilt
das Objekt als besonders zäh und wird
in die Tenured Generation verfrachtet,
wo es dann mithilfe eines klassischen
Marc-and-Sweep-Algorithmus (Major
Collection) verarbeitet wird – natürlich
mit optimierten parallelen, seriellen oder
nebenläufigen Algorithmen. Allerdings
kann nicht immer gewartet werden, bis
die maximale Anzahl von Kopieraktionen für ein Objekt in der Young Generation erreicht wurde, um es in die Tenured
Generation zu kopieren. Ist kein Platz
mehr in den Survivor Spaces oder ist ein
Objekt sogar größer als eine Survivor
Space, wird es direkt in die Tenured Generation kopiert. Zusätzlich erlauben es die
aktuellen JVMs auch, Objekte in speziellen Heap-Segmenten zu allokieren, die
nur einem Thread zur Verfügung stehen.
Das vermeidet einerseits Synchronisation
beim Heap-Zugriff und erlaubt effizientes
Aufräumen von Objekten, die nur innerhalb eines Threads verwendet werden.
Dieser Speicher wird „Thread Local Heap“
bei IBM, „Thread Local Area“ bei JRockit
genannt. Sun nennt dieses Feature „Thread-Local Object Allocation“. Es sollte
nicht mit Thread-local-Variablen in Java
verwechselt werden. Gerade bei Anwen-
Abb. 3:
Visual
GC
Screenshot
dungen, die kurzlaufende Transaktionen
ausführen, führt ein Generational Heap
zu besserem Garbage-Collection-Verhalten.
Generation Sizing – auf die Größe
kommt es an
Die Größenverhältnisse zwischen Young
und Tenured Generation sind also entscheidend für die Performance. Erzeugt
eine Anwendung sehr viel kurzlebigen
Müll und ist die Young Generation zu
klein, so wird es sehr schnell passieren,
dass kurzlebige Objekte in der Tenured Generation landen. Dort kommt es
dann zu gehäuften Major Collections,
die auf die Performance drücken können. Es empfiehlt sich daher, gerade bei
Webanwendungen die Standardeinstellungen der Generationen zu kontrollieren und wenn nötig anzupassen, um die
Performance zu verbessern. Bei der Sun
JVM gibt beispielsweise der Parameter
–XX:NewRation=3 ein Verhältnis von
1:3 für Young:Tenured Generation an –
© Software & Support Verlag GmbH
javamagazin 8|2009 3
Sonderdruck
Abb. 4:
Garbage-CollectorVerhalten und
Antwortzeiten
d. h. die Young Generation belegt einen
Viertel des mit –Xmx angegebenen maximalen Heaps. Je nach JVM (Server oder
Client) und Rechnerarchitektur kann das
eine deutliche Vergrößerung gegenüber
der Standardeinstellung bedeuten und
nicht selten eine erhebliche Reduktion
des GC Overheads. Aufpassen muss man
allerdings, dass die Tenured Generation
nicht zu klein wird – gerade wenn Caches
und Sessions groß sind und schon einen
großen Teil des Speichers belegen. Auch
dann kommt es zu erhöhter GC-Aktivität, die durch den hohen Füllgrad mit
lebenden Objekten auch noch deutlich
länger für eine Mark oder Compaction-
Feedback und Fragen
zum Thema Performance
Im Rahmen der Performanceserie laden wir Sie ein, Fragen und
Feedback an uns zu richten. Wir
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 performance@
javamagazin.de.
4 javamagazin 8|2009
Phase brauchen. Webanwendungen und
transaktionale Anwendungen im Allgemeinen profitieren von der Verwendung
des Thread Local Heaps. Während diese
bei IBM und Oracle standardmäßig aktiviert sind, müssen sie bei der Sun JVM mit
-XX:+UseTLAB (vor Java 5 mit useTLE)
aktiviert werden. Der G1 Garbage Collector verwendet diese standardmäßig.
OutOfMemory trotz freien
Speichers?
Wie kann es sein, dass man einen OutOfMemory-Fehler bekommt, obwohl man
scheinbar noch genug Speicher frei hat?
Eine Möglichkeit wurde bereits am Anfang des Artikels erwähnt: Durch Fragmentierung wurde der Speicher so stark
zerteilt, dass nicht mehr genug am Stück
verfügbar ist und die Anforderungen für
eine Objektallokation nicht erfüllt werden
können. Bei einigen Implementierungen
(z. B. Sun) kann aber auch ein zu großer
Garbage Collection Overhead dazu führen, dass ein OutOfMemory-Fehler geworfen wird. Diese Funktion soll verhindern,
dass eine Applikation mit zu wenig verfügbarem Heap weiterläuft, mehr als 90 %
mit Müllbereinigung zubringt und dabei
weniger als 2 % des Speichers aufräumen
kann. In dieser Situation sollte man über
ein Resizing des Heaps nachdenken oder
© Software & Support Verlag GmbH
mithilfe eines Heapdumps analysieren,
warum und womit der Speicher belegt ist.
Wenn nötig müssen Caches verkleinert
oder die Session aufgeräumt werden.
Überwachung der Garbage
Collection
Den Heap und den Garbage Collector zu
überwachen ist unbedingt notwendig, um
die richtige Einstellung für die Generationen und die Heap-Größe im Ganzen zu
ermitteln. Im Prinzip ist eine laufende Anwendung die beste Möglichkeit zu validieren, ob man die Parameter anpassen muss
oder nicht. Jeder JVM-Hersteller bringt
seine eigenen Tools mit, wobei jconsole seit
Java 5 ein Standardwerkzeug für den Heap
und Garbage Collector über JMX ist. Sun
bietet neben jconsole und jvisualvm auch
ein Tool an, das Visual GC [2] heißt und
kostenlos heruntergeladen werden kann.
Abbildung 3 zeigt einen Screenshot von
Visual GC im Einsatz. Neben der Anzeige, wie die Auslastung der einzelnen Generationen ist, kann Visual GC auch eine
Statistik anzeigen, wie viele Objekte eine
gewisse Anzahl von Minor Collections
in der Young Generation überlebt haben.
Die ist ein guter Wert, um zu analysieren,
ob die Young Generation zu klein ist.
Wem diese Sichtweise nicht ausreicht
beziehungsweise wer lieber transaktionsorientiert arbeiten möchte, der kann auch
auf kommerzielle Produkte zurückgreifen. Diese liefern dann zusätzlich noch die
Auswirkungen der Garbage Collection
auf Antwortzeiten. In Abbildung 4 ist eine
Anwendung zu sehen, die hauptsächlich
mit Minor Collections auskommt. So genannte Runtime Supsensions – also das
Anhalten der Anwendung für Garbage
Collections – finden hier nicht statt. Aus
einer reinen Endbenutzersicht läuft diese
Anwendung gut. Allerdings ist zu erkennen, dass die Tenured Generation permanent anwächst. Da es sich hierbei um eine
einfache Webanwendung handelt, sollte
das nicht passieren. Hier kann mit entsprechendem Generation Sizing gegengesteuert werden.
Der RMI Garbage Collector
RMI nutzt einen verteilten GarbageCollector-Algorithmus, um einem ganz
natürlichen Problem von verteilten Anwendungen gerecht zu werden. Ein Remo-
www.JAXenter.de
Sonderdruck
te-Objekt auf dem Server weiß im Prinzip
nicht, ob es von seinen Clients noch referenziert wird oder nicht. Deshalb erweitert RMI den lokalen Garbage-CollectorAlgorithmus um einen verteilten Garbage
Collector (Distributed Garbage Collector
– DGC), damit bei verteilten Anwendungen der GC auch auf der Serverseite die
Objekte wegräumen kann, die nicht mehr
referenziert werden. Aus diesem Grund
führt RMI in regelmäßigen Abständen
einen GC durch, wenn das nicht schon
auf Grund regulärer GC-Tätigkeiten passiert ist. Bis Java 6 war das Intervall auf 60
Sekunden gesetzt, was häufig zu Problemen geführt hat. Gerade bei großen und
gefüllten Heaps verursachte dieses RMIGC-Intervall alle 60 Sekunden eine Major
Collection, was häufig zu einem enormen
Overhead geführt hat. Mit dem Parameter
sun.rmi.dgc.server.gcInterval kann das Intervall konfiguriert werden. Ein zu langes
Intervall ist allerdings auch gefährlich,
weil das dazu führen kann, dass RemoteObjekte nicht dereferenziert werden können, obwohl keine Referenzen mehr auf
das Objekt gehalten werden. Nur erfährt
der Server nichts davon, da die Referenz
nicht abgeräumt wird und nur dann eine
entsprechende Nachricht des DGC geschickt wird. Im Extremfall kann das auch
zu einem Out­OfMemory führen.
Die Entwicklung geht weiter: G1
G1 heißt nicht nur das Google Handy,
sondern auch ein neuer Garbage-Colletion-Algorithmus, der in Sun Java 7 Einzug
finden soll und für den es mit dem letzten Servicerelease auch schon Backports
nach Java 6 gibt. Der Algorithmus des
G1 wird „Garbage First“ genannt. Er soll
erhebliche Performanceverbesserungen
bringen, auch wenn das dahintersteckende Prinzip wiederum recht einfach
ist. Der Heap wird in eine Reihe von fixen
Teilbereichen zerlegt. Zu jedem Teilbereich wird eine Liste mit Referenzen von
Objekten gepflegt („Remember Set“), die
www.JAXenter.de
noch Objekte in dem Bereich referenzieren. Jeder Thread informiert dann den
GC, sollte er eine Referenz verändern, die
eine Anpassung der „Remember Sets“
verursachen könnte. Wird eine Garbage
Collection angefordert, werden dann zuerst die Bereiche bereinigt, die am meisten Müll beinhalten („Garbage first“). Im
besten (und wahrscheinlich nicht seltenen Fall) ist ein Bereich komplett voll mit
Müll – dann kann der Bereich einfach als
frei „definiert“ werden – ohne lästigen
Mark-and-Sweep-Algorithmus. Zusätzlich kann man beim G1 Collector Ziele
– beispielsweise den Overhead oder die
Pausenzeiten definieren. Der Collector
säubert dann immer nur so viele Bereiche, wie er in dem vorgegebenen Intervall
schaffen kann.
Fazit
GC-Algorithmen zu verstehen ist wichtig, um die Performance von Anwendungen vorhersagen und optimieren zu können. Seit der ersten JVM haben sich die
Algorithmen stetig weiterentwickelt und
verbessert. Je nach Hersteller und Version
kann man daher für seine Anwendung
alleine durch GC-Optimierungen Performance und Skalierbarkeit gewinnen,
und das, ohne Code zu verändern. Der
Optimierung der Garbage Collection
sollte ähnliche Bedeutung wie der Optimierung einer Datenbank zugemessen
werden. Das bedeutet, dass es einen Verantwortlichen im Projekt geben muss,
der die spezifischen Eigenschaften der
verwendeten JVM kennt. Zudem sind
entsprechende Lasttests notwendig, um
verschiedene Konfigurationen zu testen.
Der damit verbundene Aufwand steht auf
jeden Fall dafür. In einigen Fällen konnten mithilfe einer Optimierung der Garbage-Collection-Strategie die Antwortzeiten um bis zu 25 % verbessert werden.
Moderne Diagnosewerkzeuge erlauben
zudem den Anteil der Garbage Collection an den Responsezeiten unmittelbar
zu ermitteln.
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 Performancemanagementlösungen.
Mirko Novakovic ist Mitgründer der codecentric GmbH. Neben der Geschäftsleitung liegen seine Schwerpunkte im Bereich Performancetuning,
Java-EE-Architekturen und Open-Source-Frameworks.
Links & Literatur
[1] Sun Java HotSpot VM: http://java.sun.com/javase/technologies/hotspot/
[2] VisualGC: http://java.sun.com/performance/jvmstat/
[3] The Truth about Garbage Collection: http://java.sun.com/docs/books/
performance/1st_edition/html/JPAppGC.fm.html
[4] Oracle JRockit Tuning the Memory Management:
http://edocs.bea.com/jrockit/geninfo/diagnos/memman.html
[5] Sun Java System Application Server Enterprise, Edition 8.1 2005Q1, Performance
Tuning Guide
[6] Chapter 4, Tuning the Java Runtime System:
http://docs.sun.com/source/819-0084/pt_tuningjava.html#wp56995
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 8|2009 5
Herunterladen