Feintuning der Speicherbelegung von Java-Programmen mit visualgc

Nicht selten hängen Kernprozesse eines Unternehmens von der Verfügbarkeit von Business-Applikationen ab. Ein Java-Entwickler, der Wert auf Tempo und Langlebigkeit eines Prozesses legt, tut gut daran, ihm mit visualgc auf den Zahn zu fühlen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 13 Min.
Von
  • Christian Pemsl
  • Michael Renner
  • Alexander Neumann
Inhaltsverzeichnis

Java erfreut sich bei Business-Anwendungen großer Beliebtheit. Nicht selten hängen Kernprozesse eines Unternehmens von der Verfügbarkeit solcher Applikationen ab. Ein Entwickler, der Wert auf Tempo und Langlebigkeit eines Prozesses legt, tut gut daran, ihm mit visualgc auf den Zahn zu fühlen.

Java steht im Ruf, zwar mächtig und erweiterbar zu sein, doch unterliegt jedes in Java geschriebene Programm dem Generalverdacht der mangelnden Geschwindigkeit. Während mit frühen Java-Versionen programmierte Anwendungen tatsächlich deutlich langsamer liefen als beispielsweise eine in C oder C++ geschriebene Applikation, hat Sun spätestens mit Java 5 die Performance deutlich erhöht. Heute ist, von ungeschicktem Programmcode abgesehen, die Ursache langsamer Java-Programme meist darin zu suchen, dass die unzähligen Parameter zur Speicherbenutzung und Garbage Collection nicht optimal gewählt wurden.

Anders als bei anderen Hochsprachen müssen sich Java-Programmierer nicht um Speicherreservierung und -freigabe kümmern. Das übernimmt die Virtual Machine (VM). Somit stellt sich dem Entwickler die Aufgabe, sie hinsichtlich der Speichernutzung zu optimieren und die Freigabe nicht länger benötigter Objekte zu erreichen.

Die Voreinstellungen der Virtual Machine ermöglichen kaum mehr als grundsätzliches Funktionieren. Beim Start der Anwendung angegebene Parameter verbessern die Laufzeit. Am bekanntesten sind sicherlich Xms für den Anfangswert des Heap-Speichers sowie Xmx, das seine Maximalgröße festlegt. Weitere Parameter bestimmen, wie die VM mit Speicher umgeht und die Funktionsweise der Garbage Collection (GC), also die Freigabe nicht mehr benötigten Speichers. Die wichtigsten Optionen führt die folgende Tabelle auf.

Optionen zur Speicherverwaltung
Option Erklärung
-server JIT- und HotSpot-Optimierung für Server-Anwendungen JIT- und HotSpot-Optimierung für Client-Anwendungen
-client
-Xms=Xm Anfangsgröße des Heap (Young Generation + Old Generation) Maximale Größe (Anfangs- + Virtual-Bereich) des Heap
-Xmx=Xg
-XX:NewSize= Anfangsgröße der Young Generation (Eden + Survivor Space 1 + Survivor Space 2)
-XX:MaxNewSize= Maximale Größe der Young Generation
-XX:SurvivorRatio= Verhältnis Eden/Summe: hoher Wert -> Survivor wird kleiner; kleiner Wert -> Eden befördert Objekte direkt in die Old Generation
-XX:PermSize= Anfangsgröße der Permanent Generation
-XX:MaxPermSize= Maximale Größe der Permanent Generation (Anfangs- + Virtual-Bereich)
-XX:MinHeapFreeRatio=X Heap wird erweitert, sobald weniger als X Prozent freier Speicher zur Verfügung steht.
-XX:MaxHeapFreeRatio=X Heap wird verkleinert, sobald mehr als X Prozent freier Speicher zur Verfügung steht.
-XX:+UseParallelGC Verwendung der parallelen Garbage Collection
-XX:+UseParNewGC Verwendung der neuen Methode zur parallelen Garbage Collection
-XX:+UseParallelGCThreads= Anzahl der GC Threads bei den beiden parallelen GCs
-XX:+UseConcMarkSweepGC Verwendung der Concurrent mark-sweep (CMS) Garbage Collection
-XX:CMSInitiatingOccupancyFraction=X CMS GC startet bei X Prozent (Default-Wert 68 %)
-XX:MaxTenuringThreshold= Anzahl der GC-Läufe, die ein Object in der Young Generation verbleiben darf.
-XX:MaxGCPauseMillis= Max. Dauer, in der die Anwendung zur GC zu stoppen ist (default ist unendlich). Bei Angabe von 0 lassen sich die Objekte direkt in die Old Generation befördern.

Zusammensetzung der Speicherbereiche (Abb. 1)

Die Zusammensetzung der einzelnen Speicherbereiche eines Java-Prozesses zeigt Abbildung 1. Eine 32-Bit-JVM kann maximal 4 GByte belegen, das umfasst den Heap einschließlich der Bereiche des Perm- und Virtual-Speichers.

Bei aufwendigen Applikationen lässt sich der Ressourcenbedarf meist nicht abschätzen. Es hilft jedoch nicht, der VM möglichst viel Speicher zuzuteilen, da sie beispielsweise während der GC viel Zeit für die Bereinigung nicht benötigter Bereiche verschwendet. Deshalb muss man durch eine Analyse die optimalen Betriebsparameter für ein gegebenes Programm ermitteln.

Java nimmt dem Entwickler mit der eingebauten Garbage Collection viel Arbeit ab. Fehler durch eine verfrühte oder vergessene Freigabe nicht mehr benötigter Objekte gehören der Vergangenheit an. Das zieht allerdings Nachteile nach sich. So ist kaum zu beeinflussen, wann das System die GC durchführt. Harte Echtzeitanforderungen sind deswegen mit Java nur selten zu erfüllen. Abhängig vom eingesetzten GC-Algorithmus lässt sich eine Programmunterbrechung durch eine parallele GC zumindest in Grenzen verhindern.

Java hat im Lauf der Zeit mehrere Verfahren für die GC benutzt. Zwar sind neuere Umsetzungen den älteren meist überlegen, in manchen Fällen mag es jedoch lohnen, einem der älteren Algorithmen den Vorzug zu geben. Informationen über die verfügbaren Verfahren liefert Suns GC-FAQ. In der Praxis führt das schnelle Experimentieren mit den drei Parametern -XX:+UseParallelGC, -XX:+UseParNewGC und -XX:+UseConcMarkSweepGC ebenso zum Erfolg. Informationen darüber, wann eine CG läuft und wie lange sie dauert, liefert der Schalter -verbose:gc beim Programmaufruf. Die Option ist unbedingt notwendig, wenn man Logging-Optionen zur GC angibt.

Java kann GC-Läufe von Haus aus protokollieren. Die folgende Tabelle führt die für eine Analyse der CG-Läufe nützlichen Parameter auf. Darüber hinaus gibt es die Programmsammlung jvmstat, inzwischen Bestandteil von Java. Ein hilfreiches Analyse-Tool, visualgc, das die grafische Analyse der Speicherauslastung ermöglicht, ist von [1] herunterzuladen und beispielsweise nach /opt/ zu entpacken. Die stiefmütterliche Behandlung durch Sun mag der Grund sein, warum visualgc trotz seiner Nützlichkeit nahezu unbekannt ist.

Das zu analysierende Programm muss mindestens unter Java 1.4 laufen, während visualgc Java 5 oder später benötigt. Zur einwandfreien Funktion benötigt man Suns Java-Implementierung; IBMs Java-Varianten laufen nur mit anderen Analysewerkzeuge [2].

Mit visualgc lassen sich alle Informationen, die man zur Analyse der Speicherverwaltung braucht, übersichtlich zur Laufzeit darstellen.

Optionen zur CG-Analyse und -Optimierung
Option Erklärung
-verbose:gc Ausgabe jeder GC mit Speichergrößen und Dauer
-Xloggc:Pfad/Logfile Datei, in die man loggt
-XX:+PrintGCDetails Details zur GC, sobald Applikation beendet wird.
-XX:+PrintGCTimeStamps Timestamp jeder GC (ms seit dem Programmstart)
-XX:+PrintHeapAtGC, -XX:+PrintGCApplicationStoppedTime Zeit, während der die Applikation wegen GC angehalten war
-XX:+TraceClassloading Ausgabe der geladenen Java-Klassen
-XX:+TraceClassUnloading Ausgabe der aus dem Speicher entfernten Java-Klassen

Suns Application Server aus Sicht von jvmstat kurz vor einer GC (Abb. 2)

In Abbildung 2 ist ein Application Server von Sun aus Sicht von jvmstat kurz vor einer GC zu sehen. Das linke Fenster zeigt unter "Application Information" an, ob das Java-Programm noch läuft (Alive/Dead), wann es startete und welche Parameter die VM übergeben bekam. Darunter informieren drei Spalten über die momentane Größe der einzelnen Speicherbereiche. "Perm" steht für Permanentt Generation, "Old" für Old Generation (also dem Tenured Space), "Eden" für den Eden-Teil und "S0" beziehungsweise "S1" für den Survivor Space 1 und 2 als Teil der Young Generation. Das rechte Fenster (Graph) visualisiert die zeitliche Entwicklung eines Werts. In den ersten beiden Zeilen findet man die Zeit für die JVM-Kompiliervorgänge und die Zahl ge- und entladener Java-Klassen sowie die dafür benötigte Zeit. Darunter folgt die Summe der bisher durchgeführten GC-Läufe, deren Dauer und, wenn möglich, der Grund des letzten Laufs.

Im Eden-Diagramm sind der Speicherverbrauch und die Anzahl der GCs mit ihrer Dauer erkennbar. Man sieht, ob die Java-Anwendung rund läuft (langsam steigender Speicherverbrauch, circa eine GC alle zehn Sekunden). Es folgen das Verhalten der beiden Survivor Spaces auf sowie die Daten für die Old- und Permanent Generation mit ihrer Größe, der Anzahl der durchgeführten GCs und ihrer Dauer. Immer wenn im linken Fenster ein Bereich ausgegraut ist oder im rechten Fenster zwei Werte in Klammern stehen, handelt es sich um noch nicht benutzten (der größere minus dem kleineren Wert bei den Klammern), freien Speicher.

Das dritte untere Fenster erscheint nur, wenn die GC ein Survivor Age Histogram führt (nicht bei der jungen parallelen GC, also wenn man den JVM-Parameter -XX:+UseParallelGC angegeben hat). Dort wird Anzahl der Objekte für jede einzelne Generation angezeigt. Wenn ein Objekt eine GC im Eden überlebt und damit nicht in einen der Survivor-Bereiche wandert, wird seine Altersstufe hochgezählt. Die Spalte "Parameters" gibt die aktuelle maximale Alterungsstufe (Tenuring Threshold), die größtmögliche Alterungsstufe (Max Tenuring Threshold) der Objekte, die Belegungsgrenze des Survivor-Bereichs, bei dem eine GC in ihm endet, und die momentane maximale Größe der beiden Survivor-Bereiche an. Im Histogramm-Teil des Fensters repräsentieren die Grafiken (für jede Alterungsstufe gibt es eine) den kompletten Survivor Space. Die Größe der einzelnen Graphen stellt das Verhältnis der Alterungsstufe zum Rest des Survivor-Speichers dar.

Im Folgenden geht es um das Optimieren einer Applikation, in deren Code man nicht eingreifen kann, durch eine visualgc-Analyse. Gezeigt sei das aus Gründen der Einfachheit und der allgemeinen Verfügbarkeit der Testapplikation an der in Java geschriebenen Telnet-Anwendung Kava, die einen ASCII-Art-Stream abspielen soll, um etwas Last zu erzeugen. Die damit gesammelte Erfahrung lässt sich eins zu eins auf beliebige Java-Programme übertragen.

Man startet die Applikation und visualgc mit folgenden Befehlen:

java -verbose:gc -jar jta-1.1.jar towel.blinkenlights.nl
/opt/jvmstat/bin/visualgc $!

Ein Blick über visualgc und das geschwätzig ("verbose") eingestellte GC-Logging zeigt, dass der Default-Wert von 64 MByte Speicher für einen flüssigen Ablauf viel zu wenig ist. Während der Spielzeit des Streams sind 320 GCs fällig, der Prozess stoppt für insgesamt 700 ms. Eine Erhöhung des Heap bringt mehr Ruhe in den Programmablauf. Da die Anwendung offensichtlich den Survivor-Bereich nicht nutzt, kann man die Aufteilung des Young-Generation-Bereichs zugunsten des Eden Space verändern (beispielsweise -XX:SurvivorRatio=36. Alternativ lässt sich mit dem Schalter -client) der Speicherbereich des Perm-Speichers begrenzen. Durch weitere Analysen und Variationen anderer Parameter (Verwendung der parallelen Garbage Collection sowie 128 MByte Speicher für Xms und Xmx) kommt man letztlich zu 80 ms Stoppzeit bei nur 16 GCs.

Wer mit visualgc das Verhalten eines Applikationsservers wie Tomcat oder GlassFish untersuchen will, findet dort nur selten ein X-Terminal angeschlossen. Einen X-Server benötigt man, wenn visualgc unter Unix gestartet wird. Unter Umständen kann man visualgc "remote" via ssh auf dem Server starten und die X-Ausgabe auf den lokalen Rechner umlenken. Es waren jedoch Speicherfehler im lokalen X-Server zu beobachten; nach spätestens 24 Stunden belegte er sowohl unter Solaris als auch unter Linux nahezu den gesamten Hauptspeicher, sodass ein Neustart notwendig war.

Der Cygwin-X-Server unter Windows lief wesentlich robuster und hielt fast eine Woche durch, bevor Speicherprobleme auftraten. Mit dem Programm jstatd, Bestandteil der Java Standard Edition (Java SE), liefert Sun ein Tool aus, das "Remote Method Invocation"-Informationen (RMI) lokaler Java-Anwendungen via TCP-Port 1098 und 1099 im Netz zur Verfügung stellt. Damit kann man unter anderem visualgc auf einem lokalen Rechner starten und die Daten einer "remote" laufenden VM anzeigen. Voraussetzung hierfür ist, dass eine eventuell vorhandene Firewall die von jstatd genutzte RMI-Kommunikation erlaubt.

Laufen visualgc und die zu beobachtende Applikation auf demselben Rechner wird visualgc beim Start die Prozess-ID (PID) angegeben. Bei der Verwendung von jstatd ist als Parameter für visualgc "PID@Applikationsserver" anzugeben. Einen kritischen Punkt stellen die Zugriffsrechte auf eine per RMI erreichbare VM dar. Um Missbrauch zu verhindern, sollte eine Konfigurationsdatei den RMI-Zugriff einschränken. Wie man beispielhaft den Remote-Rechner "lyra" konfiguriert und visualgc lokal auf "cassiopeia" startet, zeigt die folgende Codepassage:

nobody@lyra: cat /opt/jvmstat/etc/jstatd.all.policy
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};

nobody@lyra: jstatd -J-Djava.security.policy=
/opt/jvmstat/etc/jstatd.all.policy


renner@cassiopeia: jps -l lyra.example.net
3002 /opt/j2sdk1.5.0/demo/jfc/Java2D/Java2Demo.JAR
2857 sun.tools.jstatd.jstatd

renner@cassiopeia: /opt/jvmstat/bin/visualgc 3002@lyra.example.net

Obwohl Java von Version zu Version ein besseres Laufzeitverhalten bietet, lässt sich mit händischem Tuning der Aufrufparameter die Performance um bis zu zehn Prozent steigern. Mit visualgc lassen sich die dafür nötigen Laufzeitparameter ohne Eingriffe in den Code ermitteln.

Nach einem Update auf eine andere Java-Version ist es ratsam zu prüfen, ob sich das Tempo durch neue Optionen erhöhen lässt. Weitere Tools aus dem Hause Sun (etwa JConsole und der NetBeans Profiler) bieten zusätzliche, jedoch schwieriger zu verstehende Einblicke in die Tiefen der JVM. Weiterführende Informationen zur Optimierung der GC finden sich im Artikel "Tuning Garbage Collection with the 5.0 Java Virtual Machine".

Christian Pemsl und Michael Renner
administrieren unterschiedliche Applikationsserver (ATG Dynamo, Sun, Tomcat, WebSphere, WebLogic) in einer großen Direktbank. Performance und Verfügbarkeit stehen dabei im Vordergund.

  1. jvmstat 3.0
  2. Informationen zu IBMs Java
  3. FAQ zur Sun Garbage Collection
  4. kava Telnet Application
  5. Tuning Garbage Collection with the 5.0 Java Virtual Machine

Heap: Speicherbereich, aus dem ein Programm Speicherblöcke für neue Objekte anfordern kann. Der Heap unterteilt sich in die Young- und die Old Generation. Die Young Generation setzt sich aus Eden- und zwei Survivor-Spaces zusammen.

Garbage Collection: Der Garbage Collector arbeitet zur Speicherbereinigung im Hintergrund eines Programms und löscht nicht mehr erreichbare Objekte. Dadurch gibt er dem Programm Speicher (Heap) frei.

HotSpot: Unterteilung des Speichers in Generationen, wobei junge Generationen bevorzugt behandelt werden.

Young Generation: Objekte werden im Eden Bereich der Young Generation angelegt. Überleben sie eine Garbage Collection, wandern sie in den Survivor-Bereich. Sind sie bei der nächsten Garbage Collection noch gültig, werden sie in den zweiten Survivor-Bereich übernommen.

Old Generation: Länger lebende Objekte werden in die Old Generation verschoben.

JIT: Just-In-Time (Compiler) übersetzt den Java-Bytecode oder Teile seit Java 1.3 erst dann zu Maschinencode, wenn man es benötigt. Durch Analyse des Laufverhaltens kann der JIT-Compiler Programmteile mit optimierten Parametern erneut übersetzen und so schnelleren Code erzeugen.

VM: Laufzeitumgebung mit virtuellem Prozessor, die den vom JIT Compiler interpretierten Bytecode einer Java-Applikation ausführt.

Bytecode: prozessorunabhängiger Programmcode, den der Java-Compiler erzeugt. (ane)