Kolumne

Aus der Java-Trickkiste: Hotspot-Schalter

Arno Haase

Die meisten Java-Entwickler wissen, wie man bei einer JVM die maximale Heap-Größe setzt oder – zumindest vor Java 8 – den Speicher für die Permanent Generation vergrößert. Aber Hotspot hat eine Vielzahl weiterer Einstellmöglichkeiten, und um die geht es diesen Monat.

Wozu Hotspot-Schalter?

Bevor wir tiefer einsteigen – warum sollten uns Hotspot-Schalter eigentlich interessieren? Die JVM läuft doch ganz gut, auch ohne dass wir sie umkonfigurieren, oder?

Der erste Grund ist tief sitzende Neugier. Der zweite, für mich wichtigere Grund ist, dass die Kommandozeilenschalter ein Weg sind, die interne Funktionsweise von Hotspot, seine Features und seine coolen Optimierungen besser zu verstehen.

Man kann sich z. B. bei einem neuen Java-Release ansehen, welche Schalter dazugekommen und welche weggefallen sind, und daran etwas über die Richtung lernen, in die Hotspot sich weiter entwickelt.

Welche Schalter gibt es

Womit wir bei der Frage wären: Wie findet man heraus, welche Schalter eine konkrete Hotspot-Version eigentlich hat?

Eine Möglichkeit ist es, in den Quelltexten von Java nachzusehen. Die sind ja frei verfügbar, und in der Datei globals.hpp steht die Liste aller Schalter, und zwar sogar mit menschenlesbarer Dokumentation. Ein einfacherer Weg ist der Aufruf von

java -XX:+PrintFlagsFinal -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -version.

Das gibt eine Liste aller Hotspot-Schalter aus, und zwar mit ihren Werten. Aber noch ein paar allgemeine Erklärungen, bevor wir uns die Liste näher ansehen.

Zunächst einmal übergibt man Schalter an Hotspot mit vorangestellten -XX:. Bei dem obigen übergeben wir also drei Schalter. Alle drei Schalter sind Flags, die entweder true oder false sein können. Um sie zu aktivieren, stellt man dem Namen ein Pluszeichen voran, zum Deaktivieren ein Minuszeichen. Im Beispiel werden also alle drei Schalter aktiviert.

Mit PrintFlagsFinal bringt man die JVM dazu, beim Starten eine Liste von Schaltern mit ihren aktuellen Werten auszugeben. Die beiden Schalter UnlockDiagnosticVMOptions und UnlockExperimentalVMOptions schalten zusätzliche Schalter frei, die sonst nicht sichtbar wären.

Und schließlich übergeben wir noch -version an die JVM, weil sie – genau wie immer – den Namen einer Main-Klasse erwartet und sonst eine Fehlermeldung ausgibt. Diese Fehlermeldung vermeiden wir mit -version.

Das Kommando gibt eine ziemlich lange, alphabetisch sortierte Liste mit Optionen aus (aktuell 812 auf meiner Maschine). Zu jeder Option stehen dort Typ, Name, aktueller Wert und eine Kategorisierung. Alle Schalter der Kategorien manageable und rw (für read/write) kann man übrigens zur Laufzeit über JMX ändern.

Man kann sich statt dieser vollständigen Liste mit -XX:+PrintCommandLineFlags die Liste aller Schalter mit ihren Werten ausgeben lassen, die vom Default abweichen. Das ist eine ziemlich überschaubare Liste, die z. B. in Logfiles von produktiven Systemen sehr hilfreich sein kann. Auf meiner Maschine erzeugt das z. B. den folgenden Output, wenn ich keine anderen Schalter setze:

-XX:InitialHeapSize=130101120 
-XX:MaxHeapSize=2081617920 
-XX:+PrintCommandLineFlags 
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:+UseParallelGC

Diese Werte hat Hotspot eigenständig auf Basis von Prozessor, Betriebssystem und RAM-Größe gewählt („Ergonomics“).

Tracing und Logging

Hotspot kann einen aktuellen Thread Dump ausgeben, d. h. eine Liste aller aktiven Threads mit ihrem jeweiligen Stack Trace. Man kann das z. B. über JMX auslösen, oder durch einen Aufruf von kill -3 unter Unix.

Mit -XX:+PrintClassHistogram kann man diese Thread Dumps um eine Liste aller aktuell geladenen Klassen zusammen mit der Anzahl der Instanzen anreichern. Damit kann man oft bei Speicherproblemen ohne zusätzliches Tooling einen ersten Eindruck gewinnen, wer der Speicherfresser ist.

Der Schalter -XX:+TraceClassLoading bringt Hotspot dazu, das Laden aller Klassen zu protokollieren. Und zwar inklusive Pfad zur JAR-Datei, aus der sie geladen wurden. Meist interessiert einen das nicht. Aber bei kniffeligen Classpath-Problemen, bei Anwendungen mit mehreren Class Loadern (Stichworte „EJB“ oder „OSGi“) oder wenn die Reihenfolge des Ladens von Klassen wichtig ist, kann dieser Schalter Aufschluss geben. Analog dazu gibt es den Schalter -XX:+TraceClassUnloading.

Garbage Collection

Auswahl und Tuning eines Garbage Collectors ist ein großes Thema, das den Rahmen dieses Artikels sprengt. Aber es gibt eine Reihe nützlicher Schalter, die mit GC zu tun haben und die hier nicht unerwähnt bleiben sollen.

Zunächst einmal kann man die JVM mit -XX:+PrintGCDetails dazu bringen, jede durchgeführte Garbage Collection zu protokollieren. Sie gibt dann jeweils eine Zeile mit Auslöser bzw. Umfang der GC sowie altem und neuem Füllgrad der verschiedenen Speicherbereiche und die verbrauchte Zeit aus.

Wenn man zusätzlich -XX:+PrintHeapAtGC angibt, schreibt die JVM zudem vor und nach jeder Garbage Collection detailliertere Informationen über die einzelnen Speicherbereiche. Außerdem nummeriert sie dann die Garbage Collections durch und protokolliert jeweils, wie viele Full GCs die JVM seit ihrem Start schon durchgeführt hat.

Wenn man den verbrauchten Speicher nach Java-Klassen heruntergebrochen haben möchte, kann man außerdem -XX:+PrintClassHistogramBeforeFullGC bzw. -XX:+PrintClassHistogramAfterFullGC angeben. Diese Schalter sorgen dafür, dass jeweils vor bzw. nach einer Full GC eine Liste aller geladenen Klassen mit Zahl der Instanzen ausgegeben wird (s. o.). Das erzeugt riesige Mengen an Output und kann die Performance spürbar beeinträchtigen, aber es kann eine Hilfe bei der Suche nach Memory Leaks sein.

Und der Schalter -XX:+DisableExplicitGC deaktiviert alle Aufrufe der statischen Methode System.gc(). Per Default löst ein Aufruf von System.gc() eine Full GC aus, was gerade bei großen Systemen etliche Sekunden oder sogar Minuten dauern kann, in denen das System komplett blockiert ist. Deshalb sollte man die Methode am besten gar nicht aufrufen – die JVM weiß ziemlich gut, wann eine GC nötig ist. Es gibt aber leider eine Reihe von Bibliotheken, die meinen, das besser zu können – und der Hotspot-Schalter ist ein eleganter Weg, das zu entschärfen.

Verschiedenes

Schließlich will ich noch einige Schalter vorstellen, die sich nicht so recht kategorisieren lassen. So kann man mit -XX:+PrintStringTableStatistics dafür sorgen, dass die JVM beim Beenden des Programms detaillierte Zahlen zur Ausnutzung der internen HashMap mit internen Strings ausgibt. Diese Map ist eine C++-Datenstruktur, in der alle Stringkonstanten landen sowie alle Strings, auf denen explizit die Methode intern() aufgerufen wurde. Und mit dem Hotspot-Schalter kann man sich ansehen, wie viele Strings sich dort angesammelt haben, wie groß sie sind und wie viel Speicher sie insgesamt verbrauchen.

Mit dem Schalter -XX:hashCode=… kann man festlegen, nach welchem Algorithmus die Default-Implementierung von Object.hashCode() ihr Ergebnis ermittelt. Per Default (-XX:hashCode=5) kommt ein ausgefeilter Algorithmus zum Einsatz, der möglichst gleichverteilte Hash-Codes erzeugt und Kollisionen nach Möglichkeit vermeidet.

Der Wert 2 dagegen sorgt dafür, dass Object.hashCode() immer den Wert 1 liefert. Das macht Programme zwar deutlich langsamer, aber man kann auf diese Weise testen, ob sie robust implementiert sind.

Der Wert 3 sorgt für Hash-Codes mit aufsteigender Reihenfolge, und bei Wert 4 werden die untersten 32 Bit der physischen Speicheradresse des Objekts verwendet.

Der Schalter -XX:MaxJavaStackTraceDepth=… setzt die maximale Anzahl von Einträgen in einem Stacktrace; alle weiteren Ebenen werden einfach verworfen. Der Default-Wert ist 1024 und deckt die meisten Situationen ab. Wenn man extrem tiefe Aufrufhierarchien debuggen will, kann man ihn entsprechend vergrößern. Und wenn man das letzte Quäntchen Performance aus einer Anwendung herausholen will, die häufig intern Exceptions wirft, kann man einen kleinen Wert vergeben – das Füllen des Stacktrace ist mit großem Abstand das Teuerste am Werfen einer Exception.

Und als Letztes möchte ich den in meinen Augen skurrilsten Schalter vorstellen: Mit -XX:SelfDestructTimer=… kann man eine Anzahl Minuten angeben, nach der sich die JVM automatisch selbst beendet.

Fazit

Dieser Artikel wollte in erster Linie zeigen, dass die Hotspot-JVM eine Fülle an Schaltern hat, und Interesse am Experimentieren wecken. Ich habe eine kleine Auswahl an Schaltern vorgestellt, die ich für manchmal nützlich oder zumindest interessant halte. Dabei habe ich den großen Bereich der Schalter ausgelassen, die den Hotspot-Compiler selbst betreffen. Um die wird es nämlich nächsten Monat gehen.

Geschrieben von
Arno Haase
Arno Haase
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationaler Datenbank und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: