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:
1ArrayList list = new ArrayList(); 2Thread.sleep(5000); // 5s to allow starting visualvm 3for (long l = 0; l < Long.MAX_VALUE; l++) { 4 list.add(new Long(l)); 5 for (int i = 0; i < 5000; i++) { 6 // busy wait, 'cause 1ms sleep is too long 7 if (i == 5000) break; 8 } 9}
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:
- 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.
- 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:
1jstat
Am Beispiel meines aktuell laufenden Eclipse sieht dies wie folgt aus:
1C:\Users\fla>jstat -gcutil 4188 2 S0 S1 E O P YGC YGCT FGC FGCT GCT 3 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:
1C:\Users\fla>jstat -gc 4188 2S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 31984,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:
1-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
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.
Weitere Beiträge
von Fabian Lange
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
Fabian Lange
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.