Kolumne

Aus der Java-Trickkiste: Speicher und Garbage Collection

Arno Haase

© Software & Support Media

Die Garbage Collection der Oracle-JVM ist dafür zuständig, nicht mehr benötigten Speicher wieder freizugeben. Und oft tut sie das selbst mit ihren Default-Einstellungen so gut, dass man kaum merkt, dass es sie gibt.

Dieser Artikel zeigt, wie eine Anwendung zur Laufzeit im Detail die Belegung der verschiedenen Speicherbereiche und die Aktivitäten der Garbage Collection verfolgen kann.

5643578905715ec456ec34d5version37sizefullDie Java-Trickkiste gibt es auch auf der JAX 2016 (18. – 22. April):

„Eine Stunde live in der IDE mit Neuem und Altbekanntem, Nützlichem und Überraschendem für Neulinge und alte Hasen.“

JAX-2016-Sessions von Arno Haase:

Nur bis 17. Dezember: Agile Day For Free + Frühbucherpreise + Gratis-Tablet!

 Logging

Der häufigste Weg zum Nachvollziehen der Garbage Collection ist – abgesehen von externen Werkzeugen wie JVisualVM – das Protokollieren per Kommandozeilenoption. Mit -XX:+PrintGC oder –XX:+PrintGCDetails bringt man die JVM dazu, bei jeder GC-Operation die Details auszugeben, mit –XX:+PrintGCCause sogar inklusive jeweils auslösendem Faktor.

Der Schalter -XX:+PrintHeapAtGC sorgt dafür, dass vor und nach jeder GC die verschiedenen Speicherbereiche mit maximaler Größe und aktueller Belegung ausgegeben werden. Welche Speicherbereiche das sind, hängt von der Version der JVM und den konfigurierten GC-Algorithmen ab: Die verschiedenen GC-Algorithmen teilen den Heap in Generationen auf, und mit Java 8 löst der Metaspace die Permanentgeneration ab.

Für ganz kniffelige Fälle erzeugt -XX:+PrintClassHistogramBeforeFullGC bzw. –XX:+PrintClassHistogramAfterFullGC eine Aufstellung der aktuell vorhandenen Instanzen je Klasse. Und -XX:+HeapDumpBeforeFullGC bzw. -XX:+HeapDumpAfterFullGC schreiben vor bzw. nach jeder Full GC einen kompletten Heap Dump in eine Datei.

Diese Kommandozeilenschalter sind extrem mächtig, und man kann sie leicht bei Bedarf aktivieren – sogar per JMX zur Laufzeit. Sie liefern eine Fülle an detaillierten Informationen, und es gibt gute Werkzeuge, um die so erzeugten Logfiles auszuwerten.

Selbsterkenntnis

Für typische GC-Tuning-Probleme ist dieses Logging eine gute Lösung. Der einzige Nachteil ist, dass eine Anwendung selbst keinen Zugriff auf ihre eigenen Speicher- und GC-Details hat. Das ist meist egal, aber zumindest technisch interessant – mein persönlicher Berührungspunkt damit war die Arbeit an einem System-Management-Werkzeug.

Zunächst einmal bietet die Standardbibliothek in der Klasse Runtime die Methoden totalMemory(), freeMemory() und maxMemory() an, die den aktuell von der JVM belegten Speicher, den davon freien Speicher sowie die maximale Speichergröße liefern, die die JVM belegen könnte. Dieses API stammt noch aus den Anfängen von Java und berücksichtigt in keiner Weise die Aufteilung des Speichers in Generationen, und es ist nicht dokumentiert, was die jeweiligen Werte genau bedeuten. Für eine grobe Füllstandanzeige in der Statuszeile einer GUI-Anwendung funktionieren sie aber beispielsweise ganz gut.

Speicher im Detail mit JMX

Java bietet aber über JMX („Java Management Extensions“) sehr viel detaillierteren Zugriff auf die aktuelle Speichersituation einer JVM. Das ist ursprünglich für externe Monitoring-Systeme gedacht, aber eine Anwendung kann darüber natürlich auch Informationen über sich selbst ermitteln (Listing 1).

final MemoryMXBean memBean = ManagementFactory.getMemoryMXBean ();
System.out.println (memBean.getObjectPendingFinalizationCount ());

final MemoryUsage heap = memBean.getHeapMemoryUsage ();
final MemoryUsage nonHeap = memBean.getNonHeapMemoryUsage ();

System.out.println (heap.getInit () + ", " +
    heap.getUsed () + ", " +
    heap.getCommitted () + ", " +
    heap.getMax ());
System.out.println (nonHeap);

Die Klasse java.lang.management.ManagementFactory ist der Einstiegspunkt in JMX. Sie hat neben den generischen Mechanismen, die das Erweiterbare an JMX ausmachen, statische Methoden für einige eingebaute MBeans. Eine davon ist getMemoryMXBean(), die die MemoryMXBean der JVM zurückliefert.

Bei dieser Bean kann man mit getObjectPendingFinalizationCount() abfragen, wie viele Objekte gerade auf ihren Aufruf von finalize() warten.

Interessanter sind die Methoden getHeapMemoryUsage() und getNonHeapMemoryUsage(), die Details zur Benutzung des Heap- und Nicht-Heap-Bereichs liefern. Ersteres ist dabei die Zusammenfassung aller Speichergenerationen, Letzteres sind Dinge wie Code Cache und Metaspace (Java 8) bzw. die Permanent Generation (bis Java 7).

Beide Methoden liefern MemoryUsage-Objekte zurück. Die enthalten jeweils Werte für die ursprünglich angeforderte Speichermenge („init“), die aktuell belegte Speichermenge („used“), den aktuell beim Betriebssystem belegten Speicher („committed“) sowie die Maximalgröße, auf die dieser Speicherbereich anwachsen kann („max“).

Speicherbelegung je Generation

Man kann die Speicherbelegung noch detaillierter je „Memory Pool“ abfragen. Das ist der Name, den JMX für verschiedene Kategorien von Speicher verwendet, also z. B. die unterschiedlichen Generationen im Heap oder den Metaspace. Dazu hat die ManagementFactory die Methode getMemoryPoolMXBeans() (Listing 2).

for (MemoryPoolMXBean pool: ManagementFactory.getMemoryPoolMXBeans ()) {
  System.out.println ();
  System.out.println (pool.getName () + ": " + pool.getType ());
  System.out.println ("  " + pool.getUsage ());
  System.out.println ("  " + pool.getPeakUsage ());
  System.out.println ("  " + pool.getCollectionUsage ());
}

Sie liefert eine Liste von MemoryPoolMXBean-Instanzen. Was für Memory-Pools es gibt, hängt von der Java-Version und der aktuellen Konfiguration ab, und Anwendungscode sollte sich nicht darauf verlassen. Jeder Pool liefert aber über getName() einen sprechenden Namen, und getType() gibt Auskunft, ob es sich um Heap handelt – also Speicher für Anwendungsobjekte – oder um Speicher für sonstige JVM-Daten, z. B. den Maschinencode kompilierter Klassen.

Die Methode getUsage() liefert ein MemoryUsage-Objekt mit Daten zur aktuellen Speicherbelegung. Darüber hinaus kann man mit getPeakUsage() den bisherigen maximalen Füllstand abfragen, und getCollectionUsage() liefert die Belegung nach der letzten GC – sofern der Pool überhaupt an der GC teilnimmt. Darüber hinaus haben Memory-Pool-Beans eine Reihe von administrativen Methoden. Mit resetPeakUsage() kann man z. B. die Maximalbelegung zurücksetzen.

Mit Code in der Art von Listing 2 kann eine Anwendung aber mit wenig Aufwand sehr detaillierte Einblicke in ihre aktuelle Speichersituation bekommen.

Garbage Collection

Anders als bei der aktuellen Speicherbelegung ist für ein Verständnis der GC eine Momentaufnahme nicht hilfreich. Es handelt sich ja um ein wiederkehrendes Ereignis, für das man jeweils den Zeitpunkt, die Dauer und andere spezifische Daten wissen will. Deshalb bietet JMX eine Möglichkeit an, einen Listener für GC-Events anzumelden (Listing 3).

final NotificationListener gcListener = new NotificationListener () {
  @Override public void handleNotification (Notification notification, Object handback) {
    if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
      final GarbageCollectionNotificationInfo info = 
        GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());

      System.out.println ("GC " + info.getGcInfo().getId() + ": " +
        info.getGcName() + " (" +
          info.getGcCause() + "): " +
        info.getGcAction());
      System.out.println ("  " + info.getGcInfo().getDuration () + "ms");
    }
  }
};

for (GarbageCollectorMXBean gcbean : ManagementFactory.getGarbageCollectorMXBeans()) {
  final NotificationEmitter emitter = (NotificationEmitter) gcbean;
  emitter.addNotificationListener (gcListener, null, null);
}

Der Code definiert einen NotificationListener, der für jede GC aufgerufen werden soll. Er prüft zunächst, ob ein Event eine GC Notification ist und erzeugt dann aus ihm eine GarbageCollectionNotificationInfo. Dieser Teil des Codes ist nicht portabel und verwendet eine Klasse aus dem Package com.sun; die Idee stammt von Jack Shirazi.

Dieses Infoobjekt enthält einige Daten zur GC direkt, andere hängen an einem GcInfo-Objekt, das über getGcInfo() erreichbar ist. Dort liegt z. B. die ID, eine laufende Nummer, die die JVM je Typ von Garbage Collection vergibt.

Die Methode getGcName() liefert einen Klartextnamen für den GC-Algorithmus, getGcCause() eine kurze Beschreibung für den Auslöser der GC (zu wenig Speicher, expliziter Aufruf von System.gc(), usw.) und getGcAction() in verklausulierter Form die Information, ob es sich um eine inkrementelle oder eine volle GC handelt.

Am GcInfo-Objekt kann man mit getDuration() abfragen, wie viele Millisekunden die GC insgesamt gedauert hat. Außerdem gibt es dort die Methoden getMemoryUsageBeforeGc() und getMemoryUsageAfterGc(), die für alle betroffenen Memory-Pools (s. o.) die MemoryUsage vor und nach der GC liefern.

Jetzt muss der Code diesen Listener noch bei JMX registrieren. Dazu holt er mit ManagementFactory.getGarbageCollectorMXBeans() die MBeans für alle aktiven Garbage Collectoren, castet sie auf NotificationEmitter und ruft addNotificationListener() mit dem oben definierten Listener auf.

Von diesem Zeitpunkt an löst jede Garbage Collection einen Callback in den Listener aus, der die Details ausgeben oder sonst auf beliebige Weise verarbeiten kann.

Fazit

Für ein Tuning der Garbage Collection ist das Logging mit -XX:+PrintGCDetails im Allgemeinen sehr handlich und ausreichend. Anwendungen können aber über JMX auch sehr detaillierte Informationen zur Speicherbelegung und Garbage Collection abfragen.

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
400
  Subscribe  
Benachrichtige mich zu: