Nützliche JVM Flags – Teil 7 (CMS Collector)

Keine Kommentare

Der Concurrent Mark Sweep Collector der HotSpot JVM (abgekürzt: CMS Collector) hat das Ziel, die Pausenzeiten einer Java-Anwendung gering zu halten. Dieses Ziel liegt den meisten interaktiven Anwendungen wie z.B. Webanwendungen zugrunde. Wir betrachten nun kurz die Arbeitsweise des CMS Collector sowie mögliche Gefahren bei seinem Einsatz und wenden uns anschließend den relevanten Flags zu.,

Wie der Throughput Collector (siehe Teil 6 dieser Serie) ist der CMS Collector für die GC in der Old Generation zuständig. Seine Arbeitsweise ist jedoch im Vergleich zum Throughput Collector ungleich komplexer. Der Throughput Collector pausiert die Anwendung, falls nötig auch für einen längeren Zeitraum, und kann daher ganz ungestört arbeiten. Im Gegensatz dazu ist der CMS Collector darauf ausgelegt, weitestgehend nebenläufig („mostly concurrent“) zur Anwendung zu arbeiten und dabei nur sehr wenige, kurze Pausen zu verursachen. Um dies zu erreichen, ist ein GC-Zyklus des CMS Collector in mehrere aufeinanderfolgende Phasen unterteilt.

Die sechs Phasen des CMS Collector

Ein Zyklus des CMS Collector besteht aus sechs Phasen. Vier dieser Phasen (deren Namen in der folgenden Aufzählung mit „Concurrent“ beginnen) finden nebenläufig zur eigentlichen Anwendung statt. Die verbleibenden zwei Phasen halten die Anwendungs-Threads an.

  1. Initial Mark: Die Anwendungs-Threads werden kurz angehalten, um deren Objektreferenzen einzusammeln. Danach werden die Anwendungs-Threads wieder gestartet.
  2. Concurrent Mark: Ausgehend von den Objektreferenzen aus Phase 1 werden alle anderen noch referenzierten Objekte traversiert.
  3. Concurrent Preclean: Möglichst viele der Änderungen an Objektreferenzen, die von den Anwendungs-Threads während Phase 2 vorgenommen wurden, werden nachvollzogen.
  4. Remark: Da die Anwendungs-Threads noch laufen, kann sich auch während Phase 3 immer noch etwas ändern. Daher werden die Anwendungs-Threads nun noch einmal kurz angehalten, um eventuelle weitere Änderungen an Referenzen zu überprüfen und vor Beginn des Aufräumens einen sicheren Zustand zu garantieren. Denn es gilt unter allen Umständen zu vermeiden, dass die GC vermeintlich tote Objekte entfernt die aber noch referenziert werden
  5. Concurrent Sweep: Die nicht mehr referenzierten Objekte werden vom Heap entfernt.
  6. Concurrent Reset: Es werden interne Aufräumarbeiten durchgeführt, damit der nächste GC-Zyklus sauber beginnen kann.

Im Vergleich zu den vier nebenläufigen Phasen sind die beiden Stop-the-World-Phasen sehr kurz. Dennoch ist es wichtig zu verstehen, dass der CMS Collector eben nicht komplett nebenläufig zur Anwendung arbeitet, sondern nur fast.

Zu erwähnen ist noch, dass auch beim Einsatz des CMS Collector die Young Generation mit einem Stop-the-World-Ansatz behandelt wird, analog zum Throughput Collector. Das Argument hierfür ist, dass die Young Generation GC üblicherweise so kurz ist, dass die Pausenzeiten auch für interaktive Anwendungen klein genug sind.

Gefahren beim Einsatz

Es gibt vor allem zwei Gefahren beim Einsatz des CMS Collector, aus denen sich in der Praxis regelmäßig Herausforderungen für gezieltes Tuning ergeben:

  1. Heap-Fragmentierung
  2. Zu schnelle Objektallokation

Eine Heap-Fragmentierung kann auftreten, weil der CMS Collector im Gegensatz zum Throughput Collector keinerlei Mechanismen zur Defragmentierung des Heap verwendet. Es kann daher während des Lebenszyklus einer Anwendung der Fall eintreten, dass die Old Generation zwar in Summe noch über jede Menge freien Heap-Speicher verfügt, aber keiner der einzelnen Speicherbereiche für sich groß genug ist, um ein bestimmtes Objekt komplett aufzunehmen. In dieser Situation gibt es für den CMS Collector keinen Ausweg mehr. Um die Fragmentierung zu beseitigen, führt die JVM dann eine Full GC mit dem Throughput Collector durch. Diese löst zwar die Fragmentierung auf, hält allerdings auch die Anwendung komplett an. Je nach Art der Anwendung kann schon ein einmaliges Auftreten dieser Situation höchst unangenehm sein. Es ist daher wichtig im Hinterkopf zu haben, dass beim CMS Collector trotz der Nebenläufigkeit die Gefahr einer Full GC (und damit einer sehr langen Pausenzeit) immer noch existiert. Diese Gefahr ist „by design“ und lässt sich nicht wegkonfigurieren.

Die zweite Gefahr ist eine zu schnelle Objektallokation. Mit zu schneller Objektallokation ist gemeint, dass die Rate, mit der die Anwendung neue Objekte alloziert, höher ist als die Rate, mit welcher der CMS Collector Objekte vom Heap entfernt. Kurz gesagt: Die nebenläufige GC kommt mit dem Aufräumen nicht nach. Irgendwann tritt dann der Fall ein, dass in der Old Generation kein Platz mehr für ein neues Objekt ist, weil sich dort noch zu viele tote (aber noch nicht eingesammelte) Objekte befinden. Man spricht dann von einem „concurrent mode failure“. In so einer Situation reagiert die JVM genau wie bei der Heap-Fragmentierung: Sie löst eine Full GC mit dem Throughput Collector aus, um schnellstmöglich Platz für das betreffende Objekt zu schaffen. Eine zu schnelle Objektallokation hat also letzten Endes dieselben unschönen Konsequenzen wie eine Heap-Fragmentierung.

Manifestiert sich eine dieser Gefahren in der Praxis (was in der Regel erst auf einem Produktivsystem passiert), so ist die Ursache häufig, dass unnötig viele Objekte in der Old Generation landen. Da der Fokus dieser Serie nicht auf dem GC-Tuning liegt, seien nur kurz zwei vielversprechende Ansätze zur Bekämpfung dieser Ursache erwähnt. Zum einen kann die Young Generation vergrößert werden. Das Ziel dieser Maßnahme ist, dass kurzlebige Objekte nicht voreilig in die Old Generation befördert werden, sondern ihnen genug Zeit bleibt in der Young Generation eingesammelt zu werden (siehe hierzu auch Teil 5 dieser Serie). Zum anderen kann die Anwendung mit Hilfe von Heap Dumps oder durch Einsatz eines Profilers auf verschwenderische Objekterzeugung untersucht werden. Das Ziel dieser Maßnahme ist, die Anzahl unnötig erzeugter Objekte in der Anwendung zu reduzieren und damit die GC zu entlasten.

Im Folgenden beschäftigen wir uns nun mit den wichtigsten Flags, die zur Konfiguration des CMS Collector zur Verfügung stehen.

-XX:+UseConcMarkSweepGC

Dieses Flag muss gesetzt werden, um den CMS Collector zu aktivieren. Standardmäßig verwendet die HotSpot JVM den Throughput Collector.

-XX:+UseParNewGC

Dieses Flag aktiviert die parallele Ausführung der Young Generation GC mit mehreren Threads. Es verwundert vielleicht, dass hierzu nicht das Flag -XX:+UseParallelGC, wie beim Throughput Collector, verwendet werden kann, denn der Algorithmus für die Young Generation GC ist im Prinzip der gleiche. Der Grund für das eigene Flag -XX:+UseParNewGC ist, dass sich das Zusammenspiel mit der Old Generation GC im Falle des CMS Collector anders verhält und es deshalb zwei unterschiedliche Implementierungen des Young Generation Collector gibt.

Das Flag -XX:+UseParNewGC wird (bis auf einige ältere JVM-Versionen) automatisch gesetzt, wenn -XX:+UseConcMarkSweepGC gesetzt ist. Das hat zur Folge, dass man die parallele Young Generation GC explizit deaktivieren muss, falls man stattdessen eine sequentielle GC einsetzen möchte. Dies geschieht durch Deaktivieren des Flags mit -XX:-UseParNewGC.

-XX:+CMSConcurrentMTEnabled

Ist dieses Flag gesetzt, so werden die nebenläufigen Phasen des CMS Collector mit mehreren Threads durchgeführt. Im Ergebnis laufen dann die eigentliche Anwendung (mit potentiell vielen Threads) und der CMS Collector (ebenfalls mit mehreren Threads) parallel zueinander. Standardmäßig ist dieses Flag bereits aktiviert. Je nach Ausstattung des Systems, auf dem die JVM läuft, kann es aber sinnvoll sein, das Multithreading der nebenläufigen Phasen auszuschalten.

-XX:ConcGCThreads

Das Flag -XX:ConcGCThreads=<value> (früher auch: -XX:ParallelCMSThreads) bestimmt die Anzahl Threads, mit denen die nebenläufigen Phasen des CMS Collector parallel ausgeführt werden. Die Verwendung mehrerer Threads für die nebenläufigen Phasen kann den GC-Zyklus beschleunigen. Ist beispielsweise value=4, so werden vier Threads verwendet. Da mehrere Threads wie üblich einen gewissen Synchronisationsoverhead mit sich bringen, sollte aber im Einzelfall gemessen werden, ob die GC dadurch tatsächlich beschleunigt wird oder nicht.

Wird dieses Flag nicht gesetzt, berechnet die JVM seinen Wert automatisch aus dem Wert des Flags -XX:ParallelGCThreads, anhand der Formel ConcGCThreads = (ParallelGCThreads + 3)/4. Das Flag -XX:ParallelGCThreads hat also beim CMS Collector nicht nur einen Einfluss auf die Stop-the-World-Phasen, sondern ggf. auch auf die nebenläufigen Phasen.

Wir sehen, dass es eine ganze Reihe von Möglichkeiten gibt, die parallele Ausführung des CMS Collector zu beeinflussen. Genau deshalb empfiehlt es sich, beim Einsatz des CMS Collector zunächst die Standardeinstellungen zu verwenden und in der Folge gezielt zu messen, ob überhaupt Verbesserungsbedarf besteht. Falls ja, sollte schrittweise getestet werden, wie einzelne Änderungen an den Flags sich auf die GC-Performance auswirken.

-XX:CMSInitiatingOccupancyFraction

Der Throughput Collector nimmt seine Arbeit auf, wenn der Heap voll ist (d.h. wenn kein Platz für ein neues oder zu verschiebendes Objekt zur Verfügung steht). Beim CMS Collector wäre dieser Zeitpunkt hingegen zu spät, da die Anwendung während der nebenläufigen GC weiterläuft und fortwährend neue Objekte erzeugt. Der CMS Collector muss seine Arbeit also früher aufnehmen als der Throughput Collector, um rechtzeitig fertig zu werden.

Wann der richtige Zeitpunkt gekommen ist, einen neuen GC-Zyklus zu starten, entscheidet die JVM weitestgehend autonom, basierend auf zur Laufzeit ermittelten Statistiken zur Objekterzeugung und -bereinigung. Es ist aber möglich, der JVM einen Tipp zu geben, wann der CMS Collector zum ersten Mal aktiv werden soll. Hierzu dient das Flag -XX:CMSInitiatingOccupancyFraction=<value>, wobei value die prozentuale Auslastung der Old Generation angibt. Ein Wert von 75 heißt beispielsweise, dass der CMS Collector aktiv wird, wenn 75% des Heap-Speichers der Old Generation belegt ist. Der Standardwert für dieses Flag beträgt traditionell 68.

-XX:+UseCMSInitiatingOccupancyOnly

Hat man eine gute Kenntnis der Anwendung, so kann man mit Hilfe des Flags -XX:+UseCMSInitiatingOccupancyOnly dafür sorgen, dass die JVM auf die Berechnung von Statistiken für den CMS Collector verzichtet. Stattdessen wird der für -XX:CMSInitiatingOccupancyFraction angegebene Wert für jeden GC-Zyklus, nicht nur für den ersten verwendet.

-XX:+CMSClassUnloadingEnabled

Standardmäßig führt der CMS Collector keine GC in der Permanent Generation durch. Aktiviert werden kann die Permanent Generation GC durch das Setzen von -XX:+CMSClassUnloadingEnabled. In einigen älteren JVM-Versionen kann es außerdem erforderlich sein, zusätzlich das Flag -XX:+CMSPermGenSweepingEnabled zu setzen.

-XX:+CMSIncrementalMode

Dieses Flag aktiviert den inkrementellen Modus des CMS Collector. Dieser Modus pausiert die nebenläufigen Phasen in regelmäßigen Abständen, um die Threads der eigentlichen Anwendung so wenig wie möglich zu beeinflussen. Natürlich dauert ein GC-Zyklus insgesamt dann länger. Sinn macht der Einsatz dieses Flags daher nur, wenn der normale Modus des CMS Collector nachweislich zu sehr mit der Anwendung interferiert. Das passiert eher selten, und wenn überhaupt, dann nur bei Systemen mit wenigen Prozessoren.

-XX:+ExplicitGCInvokesConcurrent und -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

Es ist heutzutage eine (weitestgehend allgemein akzeptierte) Best Practice, auf das explizite Anfordern einer GC durch den Aufruf von System.gc() in der Anwendung zu verzichten. Dieser Ratschlag gilt sogar unabhängig vom verwendeten GC-Algorithmus. Allerdings ist eine explizite GC im Falle des CMS Collector besonders unangenehm, da sie eine Full GC (d.h. Stop-the-World) zur Folge hat. Es gibt zum Glück die Möglichkeit, diese Standardeinstellung zu beeinflussen. Mit dem Flag -XX:+ExplicitGCInvokesConcurrent können wir verlangen, dass im Falle einer explizit angeforderten GC eine nebenläufige GC ausgeführt wird. Das Flag -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses geht noch weiter und sorgt dafür, dass in diesem Fall zusätzlich die Permanent Generation ebenfalls nebenläufig bereinigt wird. Man kann sich mit diesen Flags also ein wenig gegenüber unerwarteten Stop-the-World-GCs absichern.

-XX:+DisableExplicitGC

Da wir gerade beim Thema sind: Es betrifft zwar nicht speziell den CMS Collector, aber trotzdem (oder gerade deshalb) sollte jeder das Flag -XX:+DisableExplicitGC kennen. Hiermit können wir der JVM mitteilen, dass sie jegliche explizite Anforderung einer GC komplett ignorieren soll. Dieses Flag gehört für mich zu einer Gruppe von „Standardflags“, die man guten Gewissens bei nahezu jedem JVM-Start setzen kann.

Patrick Peschlow

Dr. Patrick Peschlow ist Entwicklungsleiter bei der CenterDevice GmbH und verantwortlich für die Architektur sowie die technische Umsetzung der Anwendung. Es gibt Leute, die sagen, DevOps sei sein zweiter Vorname.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.