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:
1$ 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:
1$ 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.hprof
in dem Verzeichnis gespeichert, in dem die JVM gestartet wurde, wobei die Prozess-ID des JVM-Prozesses ist. Möchte man den Heap-Dump an einem anderen Ort speichern, kann man das mit
-XX:HeapDumpPath=
spezifizieren. Hier gibt 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.
1$ 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:
1$ 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.
Weitere Beiträge
von Patrick Peschlow
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Patrick Peschlow
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.