Nützliche JVM Flags – Teil 5

Keine Kommentare

Heute geht es um einen sehr wichtigen Heap-Bereich, nämlich die Young Generation. Bevor wir uns mit den relevanten Flags und ihren Auswirkungen beschäftigen, widmen wir uns zunächst einmal der Frage: Warum ist die Dimensionierung der Young Generation so wichtig?

Allein aus funktionaler Sicht benötigt eine JVM gar keine Young Generation. Vielmehr ist der einzige Zweck der Young Generation eine Optimierung der Garbage Collection (GC). Das Ziel der Unterteilung des Heap in Young und Old Generation ist, die Allokation neuer Objekte zu vereinfachen und die Deallokation nicht mehr benötigter Objekte möglichst effizient zu realisieren. Dabei unterscheiden sich die GC-Strategien für die Young Generation oft von denen für die Old Generation.

Umfassende Beobachtungen haben gezeigt, dass in einem typischen Programm die meisten neu instanziierten Objekte jung „sterben“, d.h. im Programmfluss nicht sonderlich lange referenziert werden. Außerdem haben Analysen ergeben, dass jüngere Objekte selten von wesentlich älteren Objekten referenziert werden. Diese beiden Punkte machen es für die GC wünschenswert, einen schnellen Zugang zu den jungen Objekten zu haben – zum Beispiel in einem separaten Speicherbereich namens Young Generation. In diesem Speicherbereich kann die GC dann „tote“ junge Objekte gezielt und schnell bereinigen, ohne dabei zu sehr auf ältere Objekte achten oder gar mühevoll zwischen diesen suchen zu müssen.

Bei der Sun/Oracle JVM besteht die Young Generation aus mehreren Teilbereichen: einem recht großen Bereich namens „Eden“ und zwei kleineren Survivor Spaces namens „From“ und „To“. In „Eden“ werden neue Objekte angelegt; kann „Eden“ aus Platzgründen ein neues Objekt nicht aufnehmen, so wird es direkt in der Old Generation angelegt. Die Survivor Spaces dienen dazu, den Aufenthalt junger Objekte in der Young Generation zu verlängern. Die in „Eden“ angelegten Objekte wechseln im Rahmen der GC zunächst in die Survivor Spaces und werden dort bis zu einem gewissen Alter verwahrt, bevor sie in die Old Generation wandern.

Aus der Erwartung heraus, dass bei einer GC üblicherweise ein Großteil der jungen Objekte gelöscht werden kann, wird eine Kopier-Strategie („Copy Collection“) verwendet. Ein Objekt in „Eden“, das bei einer GC noch referenziert wird, wird in den Survivor Space „To“ kopiert. Das Schicksal der noch referenzierten Objekte in „From“ hängt dagegen von ihrem Alter ab. Sie werden ebenfalls nach „To“ kopiert, sofern sie ein gewisses Alter („Tenuring Threshold“) nicht überschritten haben. Sind sie aber zu alt, werden sie in die Old Generation kopiert. Am Ende dieser Kopiervorgänge können „Eden“ und „From“ als leer aufgefasst werden und alle noch referenzierten Objekte der Young Generation befinden sich in „To“. Sollte „To“ an irgendeinem Punkt während dieser Prozedur voll werden, so wandern die restlichen Objekte ausnahmslos in die Old Generation – ohne Aussicht auf Rückkehr. Abschließend tauschen „From“ und „To“ noch ihre Namen bzw. Rollen, so dass „To“ für die nächste GC wieder komplett leer ist und „From“ die verbleibenden jungen Objekte enthält.

Beispiel: Die Young Generation direkt vor und nach einer Garbage Collection

Beispiel für den Ausgangspunkt und den Endzustand einer Young Generation GC. Freier Platz ist grün, nicht mehr referenzierte Objekte sind gelb und noch referenzierte Objekte sind rot gekennzeichnet. In diesem Beispiel sind die Survivor Spaces groß genug, so dass keine Objekte in die Old Generation verschoben werden müssen.

Üblicherweise wird ein Objekt also in „Eden“ geboren und alterniert anschließend bei jeder Young Generation GC zwischen den beiden Survivor Spaces. Überlebt das Objekt bis zu einem bestimmten Alter, wird es schließlich in die Old Generation verschoben und reiht sich in die Menge der langlebigen Objekte ein. Dort wird es später nur unter größerem Aufwand mit einem der schwergewichtigeren Old Generation GC-Algorithmen (eine reine Copy Collection wird hier in der Regel nicht verwendet) zu entsorgen sein.

Es wird nun klar, warum die Größe der Young Generation wichtig ist: Wählen wir die Young Generation zu klein, so werden auch kurzlebige Objekte in die Old Generation verschoben und belasten dort die ohnehin schon aufwändigere GC. Wählen wir hingegen die Young Generation zu groß, so besteht die Gefahr vieler unnötiger Kopiervorgänge für langlebige Objekte, die letzten Endes ohnehin in die Old Generation wandern werden. Einen Mittelweg zu finden erscheint hier prinzipiell wünschenswert; welche Dimensionierung für eine vorliegende Anwendung aber tatsächlich günstig ist, kann oft nur durch Messungen und gezieltes Tuning herausgefunden werden. Und hier kommen die JVM-Flags ins Spiel.

-XX:NewSize und -XX:MaxNewSize

Analog zur Gesamtgröße des Heap (mit -Xms und -Xmx) können wir absolute untere und obere Schranken für die Größe der Young Generation explizit vorgegeben.  Bei der Wahl von -XX:MaxNewSize ist zu beachten, dass die Young Generation ein Teil des Heap ist, d.h. je größer wir sie wählen desto kleiner wird wiederum die Old Generation. Aus Stabilitätsgründen ist es nicht möglich, die Young Generation größer als die Old Generation wählen, denn im schlimmsten Fall könnte es nötig sein, im Rahmen einer GC alle Objekte aus der Young Generation in die Old Generation zu verschieben. Daher ist -Xmx/2 der maximal zulässige Wert für -XX:MaxNewSize.

Die Mindestgröße der Young Generation, gesetzt durch -XX:NewSize, dient wie üblich vor allem der Optimierung. Ist das Aufkommen an jungen Objekten abschätzbar (oder wurde es sogar gemessen!), so ist es ratsam einen Wert für -XX:NewSize anzugeben, so dass die Young Generation nicht erst mühsam wachsen muss. Einen potentiellen Fallstrick gibt es noch, falls man die beiden Flags aus Versehen verwechselt: Wählt man für -XX:NewSize einen höheren Wert als für -XX:MaxNewSize, so gibt es nicht etwa eine Fehlermeldung, sondern der Wert für -XX:MaxNewSize wird entsprechend hochgesetzt. Das Ergebnis wäre also eine Young Generation mit fester, maximaler Größe, was nicht dem gewünschten Verhalten entspricht.

-XX:NewRatio

Es ist auch möglich, die Größe der Young Generation relativ zur Größe der Old Generation anzugeben. Dies hat den potentiellen Vorteil, dass die Young Generation entsprechend mitwächst bzw. schrumpft, falls die JVM die Größe des gesamten Heap zur Laufzeit dynamisch anpasst. Mit dem Flag -XX:NewRatio geben wir den Faktor an, um den die Old Generation größer als die Young Generation sein soll. So bedeutet z.B. -XX:NewRatio=3, dass die Old Generation dreimal so groß dimensioniert wird wie die Young Generation. Die Old Generation nimmt dann also 3/4 des Heap ein, während die Young Generation 1/4 des Heap-Speichers erhält.

Verwendet man sowohl absolute als auch relative Größenvorgaben für die Young Generation, so haben die absoluten Vorgaben immer Priorität. Betrachten wir ein Beispiel:

$ java -XX:NewSize=32m -XX:MaxNewSize=512m -XX:NewRatio=3 MyApp

Hier wird die JVM zwar versuchen, die Young Generation ungefähr dreimal so klein wie die Old Generation zu halten, dabei aber niemals 32 MB unterschreiten oder 512 MB überschreiten.

Es lässt sich nicht allgemein sagen, ob es vorteilhaft ist die absoluten oder relativen Größenangaben zu verwenden. Kennt man die Anwendung und deren Speicherverhalten sehr gut, so kann es von Vorteil sein, sowohl die Größe des gesamten Heap als auch die Größe der Young Generation zu fixieren. Hat man hingegen nur eine geringe oder gar keine Vorstellung vom Speicherverhalten der Anwendung, so ist es sinnvoll der JVM zunächst einmal alle Freiheiten zu geben und in der Folge zu messen, wie sich die Menge und die Lebensdauer der Objekte denn eigentlich verhält.

-XX:SurvivorRatio

Das Flag -XX:SurvivorRatio=<value> wirkt ähnlich wie -XX:NewRatio, bezieht sich jedoch auf die Bereiche innerhalb der Young Generation. Der Wert für <value> gibt an, wie groß „Eden“ im Verhältnis zu einem der beiden Survivor Spaces ist. Beispielsweise bedeutet -XX:SurvivorRatio=10, dass „Eden“ zehnmal so groß ist wie „To“ und ebenfalls zehnmal so groß ist wie „From“. Damit nimmt Eden 10/12 der Young Generation ein, während „To“ und „From“ jeweils 1/12 für sich beanspruchen. Die beiden Survivor Spaces sind immer gleich groß.

Was für einen Einfluss hat die Größe der Survivor Spaces?  Nehmen wir einmal an, dass die Survivor Spaces im Verhältnis zu „Eden“ sehr klein sind. Dann ist in „Eden“ viel Platz für neue Objekte, was auf jeden Fall wünschenswert ist. Kommt es nun zu einer GC und die allermeisten dieser Objekte können entsorgt werden, so ist „Eden“ wieder leer und alles ist wunderbar. Werden jedoch einige der Objekte noch referenziert, so gibt es in den sehr kleinen Survivor Spaces nur wenig Platz um sie aufzunehmen – viele der noch lebendigen Objekte müssen also direkt bei ihrer ersten GC in die Old Generation wandern. Nehmen wir nun umgekehrt an, dass die Survivor Spaces relativ groß sind. Dann ist in den Survivor Spaces viel Platz, um Objekte die zwar eine GC überleben aber trotzdem nicht alt werden für ihren gesamten Lebenszyklus in der Young Generation zu halten. Andererseits wird das kleinere „Eden“ schneller voll, was die Anzahl der Young Generation GCs erhöht.

Das Ziel der Konfiguration der Survivor Spaces sollte sein, die Anzahl der kurzlebigen Objekte die ungewollt in die Old Generation verschoben werden zu minimieren. Außerdem sollten Anzahl und Dauer der Young Generation GCs minimiert werden. Offenbar ist hier wieder einmal ein Kompromiss nötig, der seinerseits von der Anwendung abhängt. Um einen geeigneten Kompromiss zu finden, kann es hilfreich sein, zunächst einmal die Altersstruktur der Objekte in der Anwendung kennenzulernen.

-XX:+PrintTenuringDistribution

Das Flag -XX:+PrintTenuringDistribution führt dazu, dass bei jeder Young Generation GC die Altersstruktur der Objekte in den Survivor Spaces ausgegeben wird. Betrachten wir ein Beispiel für diese Ausgabe:

Desired survivor size 75497472 bytes, new threshold 15 (max 15)
- age   1:   19321624 bytes,   19321624 total
- age   2:      79376 bytes,   19401000 total
- age   3:    2904256 bytes,   22305256 total

Die erste Zeile sagt uns, dass die gewünschte Zielauslastung des „To“ Survivor Spaces bei ungefähr 75 MB liegt. Außerdem werden Informationen zum Tenuring Threshold, dem Richtwert für die Anzahl GCs die ein Objekt in der Young Generation verbringen darf bevor es in die Old Generation verschoben wird, ausgegeben. Konkret werden der aktuelle Wert und der maximal mögliche Wert ausgegeben; in diesem Fall betragen beide Werte 15.

In den darauffolgenden Zeilen sehen wir, wie viele Bytes von Objekten bereits wie viele GCs in der Young Generation verbracht haben. So haben ca. 19 MB bereits eine GC hinter sich, ca. 79 KB bereits zwei GCs hinter sich und knapp 3 MB bereits drei GCs hinter sich. In jeder Zeile sehen wir außerdem die Gesamtanzahl Bytes bis zu dieser Altersstufe. Der „total“-Wert in der letzten Zeile sagt uns also, dass insgesamt gut 22 MB in „To“ liegen. Da die Zielgröße für „To“ bei ca. 75 MB liegt und der aktuelle Tenuring Threshold 15 beträgt, können wir folgern, dass im Rahmen dieser GC keine Objekte in die Old Generation verschoben werden müssen. Nehmen wir nun an, die nächste GC liefert uns die folgende Ausgabe:

Desired survivor size 75497472 bytes, new threshold 2 (max 15)
- age   1:   68407384 bytes,   68407384 total
- age   2:   12494576 bytes,   80901960 total
- age   3:      79376 bytes,   80981336 total
- age   4:    2904256 bytes,   83885592 total

Vergleichen wir diese Ausgabe mit der vorherigen Tenuring Distribution. Offensichtlich befinden sich die Objekte mit einem damaligen Alter von 2 oder 3 immer noch in „To“, denn exakt die gleiche Anzahl Bytes wird nun für die Altersstufen 3 und 4 gelistet. Ebenso können wir folgern, dass ein Teil der Objekte in „To“ von der GC erfolgreich eingesammelt wurde, denn von den gut 19 MB mit damaligem Alter 1 sind nur noch gut 12 MB mit Alter 2 verblieben. Außerdem sehen wir, dass ca. 68 MB an neuen Objekten hinzugekommen sind, die nun mit Alter 1 gelistet werden.

Auffällig ist, dass die Gesamtsumme der Anzahl Bytes in „To“ – nämlich fast 84 MB – nun größer als der erwünschte Wert von 75 MB ist. Deshalb hat die JVM den Tenuring Threshold von 15 auf 2 reduziert, so dass im Rahmen der nächsten GC ein Teil der Objekte dazu gezwungen sein wird, „To“ zu verlassen. Für die davon betroffenen, aber noch referenzierten Objekte bedeutet das: Ab in die Old Generation.

-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold und -XX:TargetSurvivorRatio

Die in den Ausgaben von -XX:+PrintTenuringDistribution sichtbaren Stellschrauben können durch Flags angepasst werden. Mit -XX:InitialTenuringThreshold und -XX:MaxTenuringThreshold können wir den initialen sowie den maximalen Wert des Tenuring Threshold setzen. Mit -XX:TargetSurvivorRatio geben wir die gewünschte prozentuale Auslastung von „To“ am Ende einer GC an. Beispielsweise setzt die Kombination -XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=90 eine Obergrenze von 10 für den Tenuring Threshold und eine Zielauslastung von 90 Prozent für „To“.

Für ein zielgerichtetes Tuning mit diesen Flags gibt es verschiedene Ansätze, allerdings ist es nicht leicht allgemeingültige Richtlinien zu geben. Betrachten wir kurz zwei relativ klare Fälle.

  • Falls aus der Tenuring Distribution ersichtlich wird, dass viele Objekte einfach nur altern bis sie schließlich den maximalen Tenuring Threshold erreichen, so kann dies ein Indikator für einen zu hohen Wert von -XX:MaxTenuringThreshold sein.
  • Falls der Wert von -XX:MaxTenuringThreshold größer als 1 ist, Objekte aber niemals ein höheres Alter als 1 erreichen, so sollten wir auf die gewünschte Auslastung von „To“ schauen. Wird diese Auslastung nicht erreicht, so werden die jungen Objekte stets von der GC eingesammelt – dies ist der Optimalfall. Ist die Auslastung hingegen erreicht, so sind die Objekte in die Old Generation verschoben worden. In diesem Fall haben wir den Survivor Spaces möglicherweise zu wenig Platz zugewiesen und sollten dies korrigieren.

-XX:+NeverTenure und -XX:+AlwaysTenure

Zum Abschluss sei noch auf zwei exotische Flags verwiesen, mit denen man zwei Extreme für das Verhalten der Young Generation GC testen kann. Mit -XX:+NeverTenure können wir verlangen, dass Objekte niemals in die Old Generation verschoben werden. Das kann sinnvoll sein, wenn man sicher weiß, dass man keine Old Generation benötigt. Es verschwendet aber mindestens die Hälfte des reservierten Heap-Speichers und ist offensichtlich sehr riskant. Umgekehrt sorgt -XX:+AlwaysTenure dafür, dass junge Objekte stets unmittelbar bei ihrer ersten GC in die Old Generation verschoben werden. Auch hier ist es schwer, einen vollwertigen Anwendungsfall zu finden. Ein „Mal sehen was passiert“ in einer Testumgebung kann spannend sein, ansonsten ist aus meiner Sicht von der Nutzung dieser beiden Flags eher abzuraten.

Fazit

Die Dimensionierung der Young Generation ist wichtig und es gibt eine ganze Reihe von Flags, mit denen wir das Verhalten der Young Generation beeinflussen können. In der Regel macht es aber wenig Sinn, ausschließlich die Young Generation zu tunen ohne sich zugleich mit der Old Generation zu beschäftigen. Vor allem hängen manche der Standardwerte der beschriebenen Flags unmittelbar vom verwendeten GC-Algorithmus für die Old Generation ab. Daher sollten wir beim Heap- bzw. GC-Tuning stets auch das Zusammenspiel zwischen Young und Old Generation beachten.

In den nächsten beiden Teilen unserer Serie beschäftigen wir uns genauer mit den beiden grundlegenden GC-Strategien für die Old Generation der Sun/Oracle JVM. Sowohl für den „Throughput Collector“ als auch für den „Concurrent Low Pause Collector“ werden wir Prinzipien, Arbeitsweise und Flags kennenlernen. Unser Wissen über die Feinheiten der Young Generation wird uns dabei von Nutzen sein.

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.