|
|||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||
|
Garbage Collection Tuning - Die Ziele
|
||||||||||||||||||||||||||||||||||
In den letzten Beiträgen (siehe /
GC1
/ bis
/
GC4
/) haben wir uns im Detail angesehen, wie die verschiedenen
Garbage Collectoren in der Java Virtual Machine (JVM) funktionieren.
Diese Information braucht man für das Tuning der Garbage Collection
(GC). Deshalb werden wir uns in diesem und dem nächsten Artikel
näher mit dem eigentlichen Tuning beschäftigen. Dieses Mal wollen
wir den Kontext untersuchen, in dem Garbage-Collection-Tuning stattfindet.
Beim nächsten Mal werden wir uns die konkreten Tuning-Maßnahmen
ansehen.
Garbage-Collection-Tuning-ZieleGrundsätzlich gilt für die Garbage Collection, was ganz allgemein im Leben gilt: „Never change a winning team.“ Das soll heißen: „So lange es keine Probleme mit der Garbage Collection gibt, braucht man auch kein Tuning.“Was gibt es denn für Probleme mit der Garbage Collection? Wie kann man sie erkennen? Hierzu sollte man sich kurz vergegenwärtigen, was man mit Garbage-Collection-Tuning erreichen kann. Die Ziele sind: besserer Durchsatz, kürzere Pausen oder ein geringerer Speicherbedarf . Die Probleme, bei denen man Garbage-Collection-Tuning in Betracht ziehen soll, sind also:
DurchsatzzielDurchsatz (engl. throughput ) ist die Zeit (relativ zur Gesamtzeit), die die JVM nicht mit Garbage Collection verbringt, sondern der Anwendung zur Verfügung steht. Das ist irgendein Wert kleiner 100%, denn ganz ohne Garbage Collection geht es nicht. Die Messung des Durchsatzes sollte dabei über einen längeren Zeitraum erfolgen (die gesamte Laufzeit der Anwendung, wenn möglich), um ein realistisches Ergebnis zu erhalten, das nicht von einzelnen „Ausreißern“ verfälscht ist.Tools wie GCViewer (siehe / GCV /) können den Durchsatz aus dem Garbage-Collection-Trace (JVM Optionen: –verbose:gc –XX:+PrintGCTimeStamps ) ermitteln . Wie geht das? Der Trace besteht typischerweise aus Zeilen wie der nachfolgenden, wobei abhängig vom spezifischen Garbage Collector das Format variieren kann: 176.532: [GC 56794K->17083K(129792K), 0.1041413 secs]Dabei ist die erste Ziffer (176.532) ein Zeitstempel, der angibt, zu welchem Zeitpunkt die Garbage Collection auftritt, und die letzte Zahl (0.1041413) gibt an, wie groß die Stop-the-World-Pause (StWP) der Anwendung ist, die die Garbage Collection verursacht. Nicht nur die eigentliche Collection, sondern auch andere Stop-the-World-Pausen (z.B. Start des Concurrent Marking), sind in ähnlicher Form im Trace zu finden. In die Berechnung des Durchsatzes geht dann ein:
Bei dieser Durchsatzberechnung gibt es eine Unschärfe. Wenn man
sich die Details genauer betrachtet, sieht man, dass der CPU Verbrauch
von konkurrierenden Garbage-Collection-Aktivititäten nicht in die
Berechung eingeht. Die konkurrierenden Aktivititäten sind aber
zum Beispiel beim Concurrent-Mark-and-Sweep-Algorithmus (CMS) (/
GC4
/)
nicht völlig unerheblich. Zwar haben die Stop-the-World-Pausen des
CMS Einfluss auf den Durchsatz - keine Frage - aber die Zeiten, in denen
CMS und Applikation die CPU konkurrierend nutzen, bleiben für die
oben beschriebene Durchsatzberechnung unberücksichtigt. Dies
hat zwei Gründe. Zum einen werden diese Zeiten gar nicht im
Garbage-Collection-Trace ausgewiesen. Zum anderen sind die Berechnungsgrundlagen
nicht kompatibel: Die Zeiten, die für den Durchsatz verwendet und
im Garbage-Collection-Trace aufgelistet werden, entsprechen der abgelaufene
Zeit (engl.
elapsed time
). Für die Messung der konkurrierenden
CPU-Nutzung durch den Garbage Collector würde man aber die genutzte
CPU-Zeit (engl.
cpu time
) verwenden.
PausenzielNeben dem Ziel der Durchsatzmaximierung gibt es das Ziel der Pausenminimierung. Bei den Pausen geht es um die Stop-the-World-Pause der Anwendung, die durch die Garbage Collection verursacht wird. In unserm Beispiel176.532: [GC 56794K->17083K(129792K), 0.1041413 secs]ist es die Pause von 0.1041413 sec. An sich sind die Pausen unvermeidlich und kein Problem. Zum Problem werden sie erst dann, wenn sie so lang sind, dass sie sich störend bemerkbar machen. Solche langen Pausen können zum Beispiel zu einer “stotternden“ Benutzeroberfläche oder Timeouts in der Kommunikation führen. Das Programm wirkt dabei so, als würde es nicht oder nur zögerlich reagieren. Hat man den Verdacht, dass das Programm in einer solchen Situation nicht läuft, weil es Garbage Collection macht, dann wird man sich einen korrespondierenden Garbage-Collection-Trace genauer ansehen. Man wird im Trace die maximale Pause sowie weitere lange Pausen suchen und anschließend prüfen, ob eine dieser Pausen zeitlich mit dem Auftreten des Problems (stotternde Benutzeroberfläche oder Timeout) korreliert. Hat man auf diese Weise den Verdacht erhärtet, dass eine lange Stop-the-World-Pause die Ursache für das Problem ist, so wird man mit dem eigentlichen Tuning starten. Was man für ein solches Pausen-Tuning tut, besprechen wir im nächsten Artikel. Relevanz der Garbage Collection Tuning ZieleKommen wir noch mal auf die Tuningziele zu sprechen. Das sind wie oben bereits gesagt: ein hoher Durchsatz, konsistent kurze Pausen, oder ein möglichst geringer Speicherbedarf des Programms. Tuning heißt nun, zwei dieser Ziele konstant zu halten und das dritte durch Anpassen der Garbage-Collection-Konfiguration zu verbessern. Sollte diese Optimierung nicht ausreichen, um das Tuningziel zu erreichen, wird man einen oder beide vorher konstant gehaltenen Parameter aufgeben müssen. Das kann zum Beispiel bei einem Tuning mit Ziel „minimale Pausenzeit“ bedeuten, dass man nur noch den Durchsatz konstant hält und einen höheren Speicherverbrauch akzeptiert; man steckt hardwaremäßig mehr Speicher und teilt ihm mit –Xmx dem Java-Programm zu, um im Gegenzug konsistent kurze Pause beim Ablauf des Programms zu erhalten. Dieses Dreiecksverhältnis der Tuningziele haben wir in Abbildung 1 dargestellt. Das Dreieck mit den drei Zielen an seinen Eckpunkten ist natürlich nur eine Abstraktion. In der Praxis gibt es Unstetigkeiten, z.B. beim Umstieg von einer 32 Bit JVM auf eine 64 Bit JVM; dazu später mehr. Aber als mentales Bild, das einem hilft zu bestimmen, wohin man sich mit welchen Mitteln bewegen kann, ist es eine recht gute Basis.ZusammenfassungIn diesem Beitrag haben wird uns angesehen, wie die serielle und parallele Garbage Collection auf der Old Generation funktionieren. Weil die Old Generation andere Eigenschaften als die Young Generation hat, benutzt man keinen Mark-and-Copy-Algorithmus wie auf der Young Generation, sondern einen Algorithmus, der berücksichtigt, dass in der Old Generation nur wenige Objekte unerreichbar werden. Ein Algorithmus dafür ist der Mark-and-Compact-Algorithmus; er reorganisiert den Old-Generation-Bereich mittels einer Kompaktierung. Da dieser Algorithmus die Threads der Anwendung anhalten muss, führt er zu Stop-the-World-Pausen. Diese Pausen können bei der seriellen Variante des Mark-and-Compact-Algorithmus relativ lang sein, weil nur ein einziger Garbage-Collection-Thread die Arbeit macht. Bei der parallelen Variante sind die Pausen auf einer Multi-Prozessor-Maschine etwas kürzer. Eine weitere Alternative für die Garbage Collection auf der Old Generation, die die Pausenzeiten noch weiter verkürzt, ist der konkurrierende .Mark-and-Sweep-Algorithmus, den wir uns im nächsten Beitrag ansehen werden.Abbildung 1: Garbage Collection Tuningziele Schauen wir uns kurz noch mal an, wann welche Tuningziele relevant sind. DurchsatzFangen wir mit dem Durchsatz an. Hoher Durchsatz klingt zwar gut; Garbage-Collection-Optimierungen, um ihn zu erreichen, spielen aber in der Praxis meist keine so große Rolle. Zum einen kann man sich durch die Optimierung des Durchsatzes sehr lange Pausen einhandeln. Das heißt, aggressive Durchsatzoptimierung ist meist nur bei reinem Batchbetrieb sinnvoll, weil dort niemand durch die auftretenden langen Pausen irritiert wird. Zum anderen ist bei der Durchsatzoptimierung nicht so besonders viel zu holen. Vielleicht schafft man es mit Mühe, einen Durchsatz von 85% auf 93% zu heben. Aber das ist nur eine Steigerung um 10%. Wahrscheinlich ist eine schnellere CPU der kostengünstigere Weg, der zudem auch noch eine größere Performancesteigerung ermöglicht, als man sie mit Garbage-Collector-Tuning erreichen könnte. Die schnellere CPU führt zwar nicht zu einem besseren Durchsatz, aber die Durchsatzoptimierung macht man ja, um die Performance der Anwendung zu steigern, indem man ihr mehr CPU-Zeit auf Kosten der Garbage Collection verschafft. Eine schnellere CPU erbringt die zusätzliche Performance für die Anwendung auf andere Art: durch eine höhere Taktrate. Möchte man – unabhängig von einer schnelleren CPU - den Durchsatz erhöhen, dann stehen mehrere Garbage Collectoren zur Verfügung, die richtig eingesetzt, einen guten Durchsatz liefern. Zum Beispiel der in einem vorhergehenden Artikel (/EFF2/) von uns beschriebene Parallel Young Garbage Collector steht im Ruf ein guter Throughput Collector zu sein. Zum konkreten Tuning im nächsten Artikel mehr.PausenKommen wir nun zu dem Ziel (konsistent) kurzer Pausen. Hier ergibt sich häufig der Einstieg, um sich mit Garbage Collection näher auseinanderzusetzen. Denn wenn die Anwendung auf Grund langer Pausen „stottert“, kommt man um eine Untersuchung des Garbage-Collection-Verhaltens nicht herum. Man wird wie oben bereits beschrieben einen Garbage-Collection-Trace von der Situation erstellen und darin die langen Stop-the-World-Pausen suchen, die mit dem Stottern der Anwendung korrespondieren. Diese Pausen wird man genauer analysieren, um zu klären, was der Garbage Collector im Detail in einer solchen Situation tut. Ob die Lösung des Problems dann ein Garbage-Collection-Tuning mit dem Ziel “konsistent kurze Pausen“ ist, hängt vom Ergebnis der Analyse ab. Häufig findet man nämlich im Rahmen der Analyse Design- bzw. Implementierungsfehler der Anwendung, die die eigentlichen Ursachen des Problems sind.Hier ein Beispiel für eine solche Situation: Wir haben es uns schon einmal erlebt, dass die langen Garbage-Collection-Pausen durch das immer wieder auftretende, lang andauernde Entladen von Klassen verursacht wurde. Im Allgemeinen ist man froh, dass Klassen wieder entladen werden, wenn sie nicht mehr gebraucht werden, denn dadurch wird der Speicherbedarf der Anwendung verringert. In diesem Fall war es aber so, dass die verdächtigen Klassen fälschlicherweise „on demand“ zur Laufzeit generiert worden waren. Auf Grund fehlenden Verständnisses für das benutzte Komponentenmodell und dessen Classloading-Konzept waren fehlerhafterweise immer wieder neue Klasse für neue Class-Loader-Instanzen generiert worden, statt die Klassen einmal vorab unter einem gemeinsamen Class Loader zu generieren und sie dann immer wieder zu verwenden. Das war ganz klar ein Design- bzw. Implementierungsfehler, der sich durch auffallend lange Garbage-Collection-Pausen bemerkbar gemacht hat, bei dem Garbage-Collection-Tuning aber nicht geholfen hätte. Wenn man ausschließen kann, dass lange Pausen durch Fehler verursacht sind, dann kann man zur Pausenreduktion einen geeigneten Garbage Collector einsetzen. Es gibt eine Reihe von Garbage Collectoren, die bei richtiger Verwendung geeignet sind, konsistent kurze Garbage-Collection-Pausen zu liefern. Das ist zum einen der bereits von uns diskutierte Concurrent-Mark-And-Sweep (CMS) (/ GC4 /). Zum anderen ist es der mit JDK 6.0.14 experimentell und mit JDK 7.0 stabil verfügbare Garbage-First (G1) Garbage Collector. Auf den G1 werden wir in einem zukünftigen Artikel genauer eingehen. SpeicherverbrauchBleibt noch als Garbage-Collection-Tuningziel der geringe Speicherverbrauch. Hier gilt, ähnlich wie bei den langen Pausen, dass Design- bzw. Implementierungsfehler die Ursache für Probleme sein können. Deshalb sollte man vor dem Garbage-Collection-Tuning eine gründliche Analyse durchführen, um Fehler, die zu erhöhtem Speicherbedarf führen, auszuschließen und damit sicherzustellen, dass Tuning die richtige Maßnahme ist. Diese Analyse hat an sich nichts mit Garbage-Collection-Tuning zu tun. Vielmehr wird man verifizieren, ob der Speicherverbrauch der Applikation plausibel ist. Das geht schon ganz gut mit frei verfügbaren Tools. Man kann zum Beispiel einen Heap Dump erzeugen und diesen mit dem Memory Analyzer MAT (/MAT/) analysieren oder den geeignet konfigurierten HRPOF-Output (/HPROF/) mit dem Tool HPjmeter (/HPJM/) genauer ansehen.Hat man so festgestellt, dass man bei Design und Implementierung keinen gröberen Fehler gemacht hat, die den Speicherbedarf unnötig in die Höhe treiben, dann wird man sich immer noch überlegen, ob der Aufwand für ein Garbage-Collection-Tuning gerechtfertigt ist. Seitdem 64-Bit-JVMs auf allen Plattformen verfügbar sind, ist Garbage-Collection-Tuning mit dem Ziel: „geringer Speicherbrauch“ kein so heißes Thema mehr. Denn man kann Probleme mit dem Speicherverbrauch auch dadurch lösen, dass man der JVM mehr Speicher zur Verfügung stellt. Dabei ist wichtig, dass die JVM ausreichend physikalischen Speicher hat; allein mit virtuellem Speicher wird man keine Anwendung performant zum Laufen bekommen, weil die JVM unter Umständen ins Swappen kommt. Es ist also erforderlich, physikalisch mehr Speicher zu einzubauen, um ein Speicherproblem zu lösen. Um den gesamten physikalischen Speicher adressieren zu können, den man heute in einen Rechner einbauen kann, braucht man gegebenenfalls eine 64-Bit-JVM. Eine 32-Bit-JVM kann zwar theoretisch 4 GByte adressieren, aber weil Betriebssystem und JVM einen Teil des Speichers für sich beanspruchen, kann die Applikation nur einen Teil der 4 GByte nutzen, z.B. unter Windows maximal etwa 1,5 bis 2 GByte. Will man der Anwendung mehr Speicher zur Verfügung stellen, braucht man eine 64-Bit-JVM. Die Verwendung von mehr physikalischem Speicher als Lösung für ein Speicherproblem ist unproblematisch, wenn sowieso schon eine 64-Bit-JVM verwendet wird, und angesichts der aktuellen Speicherpreise ist sie meist die einfachere und damit effizientere Lösung. Wenn der zusätzliche physikalische Speicher aber den Umstieg von einer 32-Bit-JVM auf eine 64-Bit-JVM erforderlich macht, dann ist die Rechung nicht mehr so einfach. Durch den Umstieg verliert die Anwendung einen Teil ihrer Performance. Adressen bei einer 64-Bit-JVM sind 64 Bit statt 32 Bit groß und das Handling von 64-Bit-Adressen ist teurer als das Handling von 32-Bit-Adressen in einer 32-Bit-JVM. Die exakten Kosten hängen von der spezifischen Hardware-Plattform und dem konkreten Programm ab. Der aus dem Umstieg von einer 32- auf eine 64-Bit-JVM resultierende Performanceverlust kann bis zu 20% betragen. Um den Performanceverlust auszugleichen, braucht man dann eine schnellere CPU und so können zu den Kosten für den Speicher noch die Kosten für eine schnellere CPU dazukommen Multicore/Multiprocessor Architektur und Garbage CollectionNeben der Einführung der 64-Bit-Architekturen hat auch ein anderer Hardware-Trend, nämlich der zu Multicore-Prozessoren, einen Einfluss auf das Memory Management der JVM und damit auch auf die Garbage Collection. Die Verfügbarkeit von immer mehr Kernen in einem Prozessor führt dazu, dass immer mehr Threads parallel ausgeführt werden können. Die Markteinführung von Octocore- (also 8-Kern-) Prozessoren stehen kurz bevor. Nimmt man dazu einen mittleren Server mit vier Prozessoren, so ergibt das 32 Kerne, die parallel arbeiten können. Berücksichtigt man dann noch eine Technologie wie Hyper-Threading, so kommt man sogar auf 64 Threads, die echt parallel ausgeführt werden können. Eigentlich klingt das doch nach ziemlicher CPU-Power. Wo ist das Problem? Der Knackpunkt ist, dass in der JVM die Erzeugung von neuen Objekten deutlich besser mit der Anzahl paralleler Threads skaliert als die Garbage Collection, die den Speicher später wieder aufräumen muss. Oder knapper formuliert: je mehr Cores laufen, desto schneller wird der Heap voll. Das wiederum bedeutet, dass der Garbage Collector häufiger laufen muss. Die Folge ist, dass der Durchsatz (der ja eine relative Größe ist) bei zunehmender Anzahl von Cores im Allgemeinen sinkt.Schauen wir uns im Detail an, warum das so ist: Bei den heutigen JVMs wird jedem Thread, der neue Objekte im Heap erzeugt, ein sogenannter Thread Local Allocation Buffer (TLAB) zugeordnet, in dem er ohne Synchronisation seine Objekte erzeugen kann. Die Zuordnung der TLABs zu den Threads erfolgt dabei durch eine relativ „preiswerte“ Compare-and-Swap- (CAS)-Operation. Zusätzlich passt die JVM die Größe der TLABs abhängig von Anwendung und Hardware dynamisch während des Ablaufs des Programms an. Dabei optimiert sie die Größe so, dass zum einen der Speicherverbrauch nicht zu groß ist (zu große TLABs) und auf der anderen Seite nicht so häufig neue TLABS angefordert werden müssen (zu kleine TLABs). Für das Erzeugen von Objekten ergibt sich also, was sonst im Allgemeinen nicht gilt: die Anzahl neuer Objekte steigt (fast) linear mit der Anzahl der zur Verfügung stehenden Cores. Wie wirken sich die Multicore-/Multiprozessor-Architekturen auf der Garbage-Collector-Seite aus? Da hat sich in den letzten Jahren viel getan. Wir haben diese Entwicklung in unseren letzten Artikel schon diskutiert (siehe / GC1 / bis / GC4 /): Zum einen sind Garbage Collectoren heute in der Lage, für ihre Arbeit mehrere Threads zu nutzen ( Parallele Garbage Collection ), und zum anderen können Garbage-Collector-Aktivitäten konkurrierend zur Applikation ausgeführt werden ( Concurrent Garbage Collection ). Trotzdem reichen diese Verbesserungen bei der Garbage Collection nicht aus, um den Performancezuwachs bei der Objekterzeugung auszugleichen. Der wesentliche Grund dafür ist, dass die Synchronisation bei der Garbage Collection deutlich aufwändiger und damit „teurer“ ist als bei der Objekterzeugung. Grundsätzlich kann man an dieser Situation nichts ändern. Falls man die Möglichkeit hat, die Applikation auf mehrere JVMs aufzuteilen, so sollte man dies versuchen. Die Aufteilung führt nämlich dazu, dass sich der Synchronisationsaufwand der Garbage-Collector-Threads reduziert. Zum Beispiel: wenn die Garbage Collection nicht von 4 Garbage-Collector-Threads in einer JVM gemacht wird, sondern von je einem Garbage-Collector-Thread in 4 separaten JVMs, dann ist der Aufwand für die Synchronisation der Garbage-Collector-Threads auf Null reduziert. Mit der Aufteilung einer Anwendung auf mehrere JVMs erhält man ein Maß an Parallelität bei der Garbage Collection, das man in einer JVM allein nicht erreichen kann. Natürlich ist ein Garbage-Collection-Tuning mit Hilfe mehrerer JVMs nur bedingt einsetzbar. Die wichtigste Voraussetzung ist, dass die Applikation aus unabhängigen Teilen besteht, die sich ohne Probleme auf verschiedene JVMs verteilen lassen. ZusammenfassungIn diesem Artikel haben wir uns die wichtigsten Parameter für das Garbage-Collection-Tuning der JVM angesehen. Dies sind zum einen die Tuningziele: hoher Durchsatz, konsistent kurze Pausen und geringer Speicherverbrauch. Wir haben erläutert, warum die Kontrolle der Pausenzeiten für die meisten Anwendungen das wichtigste Ziel ist. Darüber hinaus haben wir diskutiert, wie die Hardwareentwicklungen der letzten Jahre (64-Bit Architektur, Multicore-Prozessoren) das Thema Memory Allokation und Garbage Collection beeinflusst haben. Im nächsten Artikel werden wir uns das konkrete Tuning ansehen.Literaturverweise
Die gesamte Serie über Garbage Collection:
|
|||||||||||||||||||||||||||||||||||
© Copyright 1995-2012 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/53.GC.Tuning.Goals/53.GC.Tuning.Goals.html> last update: 1 Mar 2012 |