Java Speicher – Konfiguration und Überwachung (3. Akt)

2 Kommentare

Im vorhergehenden Akt haben wir gelernt wie Java und Java Memory Leaks Speicher verbrauchen. Aus dem ersten Akt wissen wir, dass der Java Heap in verschiedene Bereiche aufgeteilt ist. Mit diesem Vorwissen wollen wir uns in diesem Akt ansehen, wie wir den JVM Speicher konfigurieren und die Nutzung zur Laufzeit überwachen können. Zwar gibt es, wie bekannt, verschiedene Bereiche, jedoch der interessanteste und relevanteste ist der Heap. In diesem Beitrag beschreibe ich die Eckpfeiler des Heaps, sowie die Möglichkeiten und Tools den Heap zu überwachen. Es gibt dazu einige JVM Befehle und insbesondere das Werkzeug VisualVM.

Heapgröße: Minimum und Maximum

Die Java Speicherverwaltung ist sehr clever. Sie versucht so wenig Speicher wie möglich zu verbrauchen und gleichzeitig zu gewährleisten, dass ausreichend Speicher zur Verfügung steht.
Zur Verdeutlichung dieses Verhaltens habe ich ein Beispielprogramm erstellt, welches nach und nach mehr Speicher verbraucht:

ArrayList list = new ArrayList();
Thread.sleep(5000); // 5s to allow starting visualvm
for (long l = 0; l < Long.MAX_VALUE; l++) {
	list.add(new Long(l));
	for (int i = 0; i < 5000; i++) {
		// busy wait, 'cause 1ms sleep is too long
		if (i == 5000) break;
	}
}

Natürlich führt es nach einiger Zeit unweigerlich zu: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Die beiden unten abgebildeten Graphen zeigen den Speicherverbrauch des Programms, gemessen mit VisualVM. Der gelbe Bereich zeigt den für Java zur Verfügung stehenden Speicher, während der blaue Bereich den aktuell genutzten Speicher darstellt. Der blaue Graph geht ab und zu zurück, da temporäre Objekte, welche bei der Vergrößerung der ArrayList erzeugt werden, durch den Garbage Collector wieder freigegeben werden können. Das linke Diagramm stellt das Standardverhalten der JVM dar. Nach Programmstart wird relativ wenig Speicher genutzt, doch nach und nach reserviert die JVM immer mehr Speicher. Es endet mit dem Maximalwert 128 Megabyte. Im Diagramm auf der rechten Seite hingegen sind die 128MB von Anfang an reserviert. Dies wird durch eine JVM Option bewirkt, welche wir gleich kennenlernen werden.

Adaptive Heap Size

Fixed Heap Size




Ein interessanter Fakt ist die Tatsache, dass wir einen OutOfMemoryError erhalten, noch bevor der Speicher vollkommen erschöpft ist. Bis zur Auflösung dieses Mysteriums am Ende dieses Artikels kann der geneigte Leser selbst darüber nachdenken.
Nun zurück zu den JVM Optionen. Es gibt hauptsächlich 2 Optionen, welche die Größe des JVM Heaps beeinflussen:

  • -Xmx128m setzt den Maximalwert auf 128 Megabyte
  • -Xms128m setzt den Startwert(Minimalwert) auf 128 Megabyte

Für die Erhöhung des Maximalwertes gibt es offensichtlich gute Gründe, doch warum sollte man den Startwert verändern wollen? Dafür gibt es im Wesentlichen zwei Argumente:

  1. Die meisten Java Anwendungen sind Serveranwendungen, welche auf exklusiv für sie bereitgestellter Hardware laufen und den gesamten Speicher belegen dürfen. Die Anpassung reduziert somit den geringen Overhead für die dynamische Veränderung der Speichernutzung.
  2. Viel wichtiger ist aber, dass durch das Abschalten der automatischen Heapvergrößerung die Beeinflussung und das Verständnis der internen Aufteilung des Heaps, welche ich als nächstes beschreiben werde, erleichtert wird. Auch Tuningmaßnahmen sind so besser abzuschätzen und durchzuführen.

Bereiche des Heaps: Tenured, Young und Permanent

Der Heap selbst ist in 3 Bereiche unterteilt: Tenured (auch Old genannt), Young (auch New genannt), und Permanent (Perm).
Wie im ersten Akt schon angedeutet, ist die Idee hinter der Aufteilung die Garbage Collection effizient gestalten zu können. So sollte der Permanent Bereich gar keine Garbage Collection benötigen (es kann aber eine konfiguriert werden), Old sollte nur selten garbage-collected werden, und im Young Bereich wird sehr viel Garbage Collection-Aktivität stattfinden.
Aus diesem Grund sind die Garbage Collection Algorithmen so entworfen, dass Young Colections möglichst schnell durchgeführt werden können. Rechts ist ein Diagramm aus der Oracle Dokumentation abgebildet, welcher die typische Altersverteilung von Objekten zeigt. Viele Objekte leben nur sehr kurz und einige für eine gewisse Zeit und überleben 2-3 Young Generation Collections (auch Minor Collections genannt). Aus diesem Grund kopiert die Young-Collection die überlebenden Objekte in sogenannte Survivor Spaces. Dort bleiben sie biss sie „alt genug“ sind und in den Old Bereich verschoben werden. Der genauen Funktionsweise ist geschuldet, dass zwei Survivor Spaces existieren, von denen einer immer leer ist. Der verbleibende Bereich der Young Generation wird Eden genannt.

Statistiken über den Heap erhalten

Wie ist nun der gesamte Speicher genau auf die Bereiche verteilt? Die JVM stellt uns verschiedene Informationen über Speichernutzung über die JMX Schnittstelle zur Verfügung. Darauf setzen verschiedene Kommandozeilenwerkzeuge auf. Hier ein Befehl der uns die aktuelle Auslastung der Speicherbereiche zeigt:

jstat

Am Beispiel meines aktuell laufenden Eclipse sieht dies wie folgt aus:

C:\Users\fla>jstat -gcutil 4188
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
  0,00   0,00   1,61  50,89  99,90     35    0,304   109   28,229   28,532

Ok, es sieht so aus als würde die JVM nicht viel unternehmen. Eden ist fast leer, Old etwa zur Hälfte gefüllt. Perm ist fast randvoll. Die GC/GCT Messwerte sind in diesem Zusammenhang uninteressant.
Doch wie groß sind die Bereiche nun? jstat -gc verrät uns dies:

C:\Users\fla>jstat -gc 4188
S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1984,0 1984,0  0,0    0,0   16256,0   263,6    40388,0    20553,3   55808,0 55752,7     35    0,304  133    34,558   34,861

Viele Zahle, alle in Kilobyte. Die Survivor Spaces sind jeweils 2MB, Eden ist bei 16MB, Old auf 40MB, Perm hat 55MB. In Summe kommt dies relativ nahe an die von Windows gemeldete Speichernutzung von 135MB. Die Differenz sind Speicherbereiche außerhalb des Heaps.

Eine ausführliche Information über alle Optionen und Ausgabespalten findet sich in der offiziellen jstat Dokumentation.

Den Heap konfigurieren

Was können wir einstellen, nun da wir wissen wie groß der Heap ist? Es existieren zwar relative viele Konfigurationsoptionen, doch werden diese eigentlich nur zur Optimierung der Garbage Collection eingesetzt. Denn alle Heap Bereiche sind gleich gut Objekte zu speichern, lediglich die Garbage Collection macht Unterschiede.

Ohne Konfiguration verwendet eine client-JVM folgende Berechnung für die Bereiche

Heap = Tenured + Young
Tenured = 2 x Young
Young = Survivor x 2 + Eden

Eine häufig durchgeführte Änderung ist die Permanent Generation mit -XX:MaxPermSize=128m zu vergrößern. Die initiale Größe kann dabei mit -XX:PermSize=64m gesetzt werden.
Je nach Anwendung kann auch die Größe von New sinnvoll angepasst werden. Dies geht entweder als Verhältnis -XX:NewRatio=2 (empfohlen, da das Verhältnis bei Größenänderung mitwächst), oder als feste Größe -XX:NewSize=128m oder -Xmn128m (einfacher zu verstehen, dafür unflexibel).
Alle Verhältnisse werden als „eins von mir – N von dem anderen“ angegeben, wobei N der für die Option genutzte Wert ist. Als Beispiel: -XX:NewRatio=2 bedeutet 33% des Heaps sind für New reserviert. 66% bleiben für Old.

Die Größe des Young Bereiches kann über -XX:SurvivorRatio verändert werden. Dies passt die Größe der Survivor Spaces an, ist aber in der Regel nicht besonders effektiv.
Die Konfiguration einer typischen Webanwendung könnte wie folgt aussehen:

-Xms2g -Xmx2g -XX:NewRatio=4 -XX:MaxPermSize=512m -XX:SurvivorRatio=6

Insgesamt wird Java etwas mehr als 2,5GB benötigen. Der Old Bereich dürfte 1,6GB groß werden, Eden 300MB und die Survivor Spaces sind etwa 50MB groß.

Den Heap überwachen

Da die von der JVM gelieferten Informationen sehr umfangreich sind, ist die Kommandozeile ein eher ungeeigneter Weg die Werte auszulesen. Im folgenden Screencast demonstriere ich 3 weitere Werkzeuge:

  • JConsole, JVM Bordmittel um JMX Metriken auszuwerten
  • Visual GC, ein super GC Visualisierungswerkzeug, auch erhältlich als Pugin für VisualVM – bestens geeignet für Troubleshootings
  • AppDynamics, hervorragendes Langzeitmonitoring Werkzeug

Unable to display content. Adobe Flash is required.

Warum tritt der OutOfMemoryError trotz freiem Speicher auf?

Mittlerweile sollte sich die Antwort auf diese Frage finden lassen. Schauen wir uns die letzte Ausgabe von Visual GC mal genauer an:

Die Old Generation ist voll. Die Survivor Spaces sind leer, und Eden ist nur zu einem Drittel gefüllt. Also erhalten wir einen OutOfMemoryError, obwohl wir noch fast 30MB Speicher von unserem Xmx Limit von 128MB frei haben.
Auch wenn wir nicht genau wissen was der Code gerade so treibt, es werden auf jeden Fall immer größer werdende Arrays erstellt welche die in der Schleife erzeugten Long Objekte aufnehmen.
An dieser Stelle passiert genau das: at java.util.Arrays.copyOf(Arrays.java:2760). Nun nehmen wir mal an, dass in Eden kein Platz mehr für das neue Array ist (in diesem Fall können wir sogar annehmen, dass das Array größer als 22 MB ist). Also würde Garbage Collection stattfinden um dafür Platz zu schaffen. Da aber in Old kein Platz mehr ist, können die Objekte aus Eden dort nicht hinverschoben werden. Darum wird der OutOfMemoryError geworfen.
Daraus kann man ableiten, dass ein OutOfMemoryError nicht bedeutet, dass der gesamte Speicher voll ist. Jedoch ist wahrscheinlich einer der Teilbereiche derart gefüllt, dass Objekte nicht mehr verschoben werden können.

In unserem nächsten Akt wenden wir uns den Heap Dumps zu, mit denen wir uns ansehen können, welche Objekte denn in unserem Heap so rumlungern.

Fabian Lange ist Lead Agent Engineer bei Instana und bei der codecentric als Performance Geek bekannt. Er baut leidenschaftlich gerne schnelle Software und hilft anderen dabei, das Gleiche zu tun.
Er kennt die Java Virtual Machine bis in die letzte Ecke und beherrscht verschiedenste Tools, die JVM, den JIT oder den GC zu verstehen.
Er ist beliebter Vortragender auf zahlreichen Konferenzen und wurde unter anderem mit dem JavaOne Rockstar Award ausgezeichnet.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Hey,

    Gratuliere zu dieser sehr übersichtlichen Ausarbeitung. Obwohl ich den Verdacht mit dem GC schon früher hatte, zeigt sich erst bei der Betrachtung mit VisualGC, wie schlimm es wirklich um den Speicher bestellt ist 😉

  • Setya, I am not sure what you are asking for.
    In my examples I used a memory leak, as it will graph nicely.
    In regular applications, objects age and get copied to old space when they are still used. First advice would be to make sure only objects are copied when they really live long.
    In Old objects resdie very long. Old gets cleaned very seldom and objects get removed when they are no longer referenced.
    GC Tuning usually aims to reduce old generation GC, as it is slower, especially in large heaps.

Kommentieren

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