Nützliche JVM Flags – Teil 4

1 Kommentar

Im Idealfall verhält sich eine neu ausgerollte Java-Anwendung mit den Standardeinstellungen der JVM ganz prima und man muss sich keine Gedanken um irgendwelche Flags machen. Leider tritt der Idealfall – gerade bei Anwendungen mit vielen Nutzern – nicht sehr häufig ein. Treten ernsthafte Performance-Probleme auf, werden die JVM-Flags schnell zum wichtigsten Mittel das uns zur Verfügung steht.

In diesem und dem nächsten Teil unserer Serie stelle ich eine Grundausstattung an Flags zum Speichermanagement der HotSpot-JVM vor. Sowohl für die Entwicklung als auch für den Betrieb von Java-Anwendungen ist die Kenntnis dieser Flags und der dahinterliegenden Konzepte sehr hilfreich.

Die etablierten Garbage Collectors der HotSpot-JVM setzen alle die gleiche Aufteilung des Heap voraus: Die Young Generation für neu erzeugte bzw. kurzlebige Objekte, die Old Generation für langlebige Objekte und die Permanent Generation für dauerhaft benötigte Objekte wie z.B. die Objektrepräsentationen der geladenen Klassen oder den Intern-Cache für Strings. Es gibt aber auch Strategien mit anderer Aufteilung, wie im Fall des derzeit noch experimentellen G1 Garbage Collectors. Im Folgenden setzen wir die „klassische“ Heap-Aufteilung mit Young, Old und Permanent Generation voraus.

-Xms und -Xmx (bzw. -XX:InitialHeapSize und -XX:MaxHeapSize)

Die wohl bekanntesten JVM-Flags aus dem Bereich der Speicherverwaltung sind -Xms und -Xmx, mit denen die anfängliche bzw. die maximale Heap-Größe angegeben wird. Beide Flags akzeptieren eine Angabe der Größe in Bytes, wobei die Größeneinheit durch einen nachgestellten Buchstaben („k“ bzw. „K“ für Kilo, „m“ bzw. „M“ für Mega oder „g“ bzw. „G“ für Giga) angegeben werden kann. Beispielsweise legt der folgende Aufruf der Java-Klasse „MyApp“ eine initiale Heap-Größe von 128 Megabyte und eine maximale Heapgröße von 2 Gigabyte fest:

$ java -Xms128m -Xmx2g MyApp

In der Praxis ist die initiale Heap-Größe auch als minimale Heap-Größe interpretierbar. Die JVM kann zwar die Größe des Heaps abhängig von dessen Auslastung dynamisch nach unten oder oben anpassen, es lässt sich aber auch bei sehr geringer Auslastung nicht beobachten dass die Heap-Größe jemals unter den mit -Xms angegebenen Wert sinkt. Für Entwickler und Anwender hat dies den Vorteil, dass man bei Bedarf eine statische Heap-Größe erzwingen kann indem man für -Xms und -Xmx denselben Wert wählt.

Die Flags -Xms und -Xmx sind übrigens nur Kürzel und werden JVM-intern auf die XX-Flags -XX:InitialHeapSize und -XX:MaxHeapSize abgebildet. Man kann diese Flags auch direkt verwenden; das Äquivalent zu obigem Aufruf sähe wie folgt aus:

$ java -XX:InitialHeapSize=128m -XX:MaxHeapSize=2g MyApp

Möchte man von einer laufenden JVM Informationen über die initiale oder die maximale Heap-Größe erhalten, z.B. durch die Angabe von -XX:+PrintCommandLineFlags auf der Kommandozeile oder durch eine JMX-Abfrage, so sollte man entsprechend nach „InitialHeapSize“ bzw. „MaxHeapSize“ Ausschau halten und nicht etwa nach „Xms“ oder „Xmx“.

-XX:+HeapDumpOnOutOfMemoryError und -XX:HeapDumpPath

Verzichtet man auf das gezielte Setzen von -Xmx, so besteht schnell die Gefahr dass man in einen OutOfMemoryError läuft – eine der unangenehmsten Erscheinungen, die einem im Umgang mit der JVM begegnen können. Wie in unserer aktuellen Blog-Serie zu diesem Thema beschrieben, muss die Ursache eines OutOfMemoryError sorgfältig diagnostiziert werden. Ein guter Start ist ein Heap-Dump, den man aber erst mal haben muss. Das kann zeitaufwändig werden, z.B. falls der OutOfMemoryError erst nach stundenlangem Betrieb der Anwendung aufgetreten ist.

Die JVM bietet hier eine leider viel zu selten genutzte Möglichkeit, bei Auftreten eines OutOfMemoryError automatisch einen Heap-Dump zu generieren und abzuspeichern. Dazu muss lediglich das Flag -XX:+HeapDumpOnOutOfMemoryError gesetzt werden. Es kostet absolut nichts und kann einem viel Zeit sparen, falls überraschend ein OutOfMemoryError auftritt. Per Default wird der Heap-Dump unter dem Namen java_pid<pid>.hprof in dem Verzeichnis gespeichert, in dem die JVM gestartet wurde, wobei <pid> die Prozess-ID des JVM-Prozesses ist. Möchte man den Heap-Dump an einem anderen Ort speichern, kann man das mit -XX:HeapDumpPath=<path> spezifizieren. Hier gibt <path> den (absoluten oder relativen) Pfad inklusive des Dateinamens für den Heap-Dump an.

-XX:OnOutOfMemoryError

Es ist sogar möglich, eine Abfolge beliebiger Kommandos auszuführen falls ein OutOfMemoryError auftritt. Denkbar wäre zum Beispiel das Versenden einer E-Mail an einen Administrator oder das Durchführen von Aufräumarbeiten. Möglich macht dies das Flag -XX:OnOutOfMemoryError, dem eine Liste von Kommandos samt Parameter mitgegeben werden kann. Mit dem folgenden Aufruf stellen wir sicher, dass im Falle eines OutOfMemoryError ein HeapDump in die Datei /tmp/heapdump.hprof geschrieben und außerdem noch das Skript cleanup.sh im Benutzer-Homeverzeichnis ausgeführt wird.

$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp

-XX:PermSize und -XX:MaxPermSize

Die Permanent Generation ist ein separater Teil des Heaps, in dem unter anderem die Objektrepräsentationen aller geladenen Klassen liegen. Insbesondere wenn die Anwendung viele Klassen laden muss, kann es nötig sein die Permanent Generation zu vergrößern um ausreichend Platz zur Verfügung zu stellen. Dies geschieht mit Hilfe der Flags -XX:PermSize und -XX:MaxPermSize. Hier gibt -XX:MaxPermSize die maximale Größe der Permanent Generation vor, und -XX:PermSize legt fest welcher Anteil davon bereits bei JVM-Start zu reservieren ist. Ein Beispiel:

$ java -XX:PermSize=128m -XX:MaxPermSize=256m MyApp

Zu beachten ist, dass die Permanent Generation aus Sicht der „Max-Flags“ nicht Teil des Heaps ist. Der mittels -XX:MaxPermSize vorgesehene Speicher wird also ggf. zusätzlich zu dem mit -XX:MaxHeapSize geforderten maximalen Heap-Speicher benötigt.

-XX:InitialCodeCacheSize und -XX:ReservedCodeCacheSize

Neben der Permanent Generation ist noch der Code Cache zu erwähnen, in dem die JVM den nativen Code kompilierter Methoden ablegt. Performance-Probleme manifestieren sich zwar häufig auf andere Weise, aber wenn es einmal den Code Cache trifft dann können die Effekte schwerwiegend sein. Ist der Code Cache voll, so gibt die JVM eine Warnung aus und begibt sich anschließend in den ausschließlich interpretierten Modus: Der JIT-Compiler wird deaktiviert und es werden keine Methoden mehr kompiliert. Das Ergebnis ist eine erhebliche Verlangsamung der Anwendung.

Wie die anderen Bereiche auch, so kann man den Code Cache dimensionieren. Hierfür werden die Flags -XX:InitialCodeCacheSize und -XX:ReservedCodeCacheSize bereitgestellt, denen man analog zu den anderen Flags Werte in Bytes zuweist.

-XX:+UseCodeCacheFlushing

Wächst der Code Cache jedoch kontinuierlich, z.B. aufgrund eines durch Hot Deployments verursachten Leaks, so hilft das Heraufsetzen der Größe nur temporär. Eine interessante und relativ neue Option ist, die JVM bei vollem Code Cache automatisch einen Teil der kompilierten Methoden entsorgen zu lassen und so neuen Platz zu schaffen. Dies kann man mit dem Flag -XX:+UseCodeCacheFlushing erreichen, das standardmäßig nicht aktiviert ist. Es kann hilfreich sein, dieses Flag zu verwenden um im Notfall das Schlimmste (nämlich eine unglaublich langsame Anwendung) zu vermeiden. Dennoch würde ich empfehlen, bei erwiesenen Performance-Problemen mit dem Code Cache so schnell wie möglich die eigentliche Ursache anzugehen, d.h. ein mögliches Leak aufzuspüren und zu beheben.

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

Kommentare

Kommentieren

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