Start - Projekte - Wissen - Impressum -
Prozesse & Systeme - Optimierung von Entwicklungsprozessen

Prinzipielles Vorgehen beim Profiling


Das Profiling erfolgt über das JVM Tool Interface (JVMTI). Ab Java 5 ersetzt diese Schnittstelle das JVMPI (JVM Profiling Interface) und JVMDI (JVM Debugging Interface). An diese Schnittstelle kann sich ein Profiling-Agent anmelden, der sich an Ereignisse der JVM bindet und die dabei anfallenden Daten sammelt. Diese Daten werden dann von einem geeigneten Tool aufbereitet und visualisert. Jedes Profiling besitzt damit die Bestandteile

  1. Starten einer JVM mit aktivierten Profiling Optionen
  2. Agent für das Abgreifen und Aufbereiten dieser Informationen
  3. Benutzerschnittstelle für die Präsentation und Analyse
Eine JVMTI Implementierung ist nicht für jede JVM vorhanden.

Zeitverhalten oder Speicherbelastung


Beim Profiling lassen sich grob zwei Sachverhalte messen und untersuchen:

  • Zeitverhalten: hier soll ermittelt werden, wie lange die JVM für die Abarbeitung von Methoden braucht, wie lange sie sich im Kontext einer Methode aufhält. Das kann man exakt aber aufwändig tun, indem einfach die Ablaufzeiten gemessen werden, oder man nimmt Stichprobem (samples) und extrapoliert die Zeiten. Bei Performanzproblemen versucht man dann die "oft besuchten Langläufer" schneller zu machen.
  • Speicherbelastung: hier ermittelt man die Belegung des Heaps mit Objektinstanzen. Insbesondere wenn sich der Heap mit der Zeit füllt und am Ende eine OutOfMemory Ausnahme auslöst, muss herausgefunden werden, warum welche Instanzen von der GC nicht beseitigt werden können.

Heapdumps mit HPROF


HPROF ist ein experimenteller Profiling Agent von Sun. Kleiner Footprint, erzeugt riesiege Dumpfiles, die mit einem Tool untersucht werden können. Geeignet für schnelle Ergebnisse auch im professionellen Bereich.

  1. JVMTI Binding: über die Startoptionen -Xrunhprof:<optionen>, -XX:+HeapDumpOnOutOfMemoryError oder -XX:+HeapDumpOnCtrlBreak oder mit Tools: JConsole ab Java 6 (Windows), JVMMon für die SAP JVM oder mit jmap ab Java 5 (Linux, Mac OS)
  2. Agent: keiner, Filedump
  3. Benutzerschnittstelle: HAT - Swing GUI zur Visualisierung der Dumpfiles, SAP Java Memory Analyzer als Eclipse Plugin oder RCP Anwendung.
Der Vorteil von HPROF: das untersuchte System wird relativ gering belastet, mit etwas Mut kann Profiling an produktiven Systemen durchgeführt werden.

Heapdumps mit JMX


Ab dem JRE 1.6 bietet JMX eine komfortable Möglichkeit für das Erzeugen von Heapdumps zur Laufzeit der Anwendung. Mit der Startoption

-Dcom.sun.management.jmxremote
lässt sich eine Anwendung remote administrieren. Dazu jconsole starten und mit dem Prozess der Anwendung verbinden. Anschließend unter MBeans com.sun.management.HotSpotDiagnostic anwählen und mit dumpHeap einen Heap-Dump erzeugen (vorher als Argument einen Namen für das Dumpfile angeben!).

Colorer


Eclipse Colorer ist ein freies Eclipse Plugin mit mittlerem Footprint. Läuft leider nur noch mit Eclipse 3.0 und wird aktuell nicht weiterentwickelt.

  1. JVMTI Binding: mit der Startoption -XrunProfilerDLL:1 -Xbootclasspath/a:<plugin>/jakarta-regexp.jar;<plugin>/profiler_trace.jar;<plugin>/commons-lang.jar (und andere Optionen)
  2. Agent: eine DLL, wird mit der Installation mit zur Verfügung gestellt und muss im PATH auffindbar sein
  3. Benutzerschnittstelle: Ein Eclipse Plugin mit Run-Configuration

TPTP bis Java1.4 - Trace and Profiling Tools Project


Bestandteil im Eclipse-Callisto Release basierend auf dem Java Virtual Machine Profiler Interface (JVMPI, experimentell für Suns JVM Implementierungen).

  1. JVMPI Binding: -XrunpiAgent:server=enabled Mit dieser Option wird das Profiling und das Sammeln von Daten von TPTP Client aus gesteuert (andere Optionen möglich)
  2. Die Hyades Data Collection Engine muss auf dem Server auf dem die untersuchte JVM läuft, gestartet werden.
  3. Benutzerschnittstelle: das TPTP Eclipse Plugin
Vorteil dieses Ansatzes: Profilingdaten werden in Echtzeit erzeugt und verarbeitet. Nachteil: das untersuchte System wird extrem belastet, für den Einsatz in Produktionsumgebungen nicht geeignet.

Installation von TPTP (lokal): (Stand: Juni 2007) In den letzten Monaten hat die Callisto-Entwicklergemeinde ordentlich einen drauf gelegt. TPTP läuft inzwischen zuverlässig und für lokales Profiling kann eine direkte Verbindung zur VM genutzt werden (anstatt einen Client oder Dienst starten zu müssen). Die Installation vereinfacht sich entsprechend.

  1. Zunächst die Profiler-Plugins installieren. Den Update Manager mit folgender Adresse versehen: http://download.eclipse.org/callisto/releases/ und "Testing and Performance" auswählen, "Select required" und installieren.
  2. Das Profiling der VM funktioniert (bei Windows) über eine DLL, die in der PATH-Variablen gefunden werden muss. Dies erreichen wir, in dem wir in der Startkonfiguration unter "Environment" die PATH-VAriable erweitern. Nun kann die Anwendung mit den Profiling-Knopf gestartet werden. Tritt dieser Fehler auf
    Error occurred during initialization of VM Could not find agent library on the library path or in the local directory: piAgent
    
    dann bitte in der Eclipse Installation unter plugins nach der Datei piAgent.dll suchen und diesen Pfad eintragen.
  3. Damit man was sieht muss nun noch ein Client gestartet werden, der die Profiling-Daten sammelt und anzeigt. Diesen Client findet man unter dem neuen Startknopf, der mit der TPTP Installation gekommen ist. Unter "Java - Attach Process" kann dort eine neue Instanz gestartet werden.
  4. Der Client findet nun automatisch Agenten (das sind VM, die gerade im Profile-Modus laufen) und bietet diese zum "attachen" an.
  5. In der "Profiling and Logging Perspective", die es nun gibt und beim Starten des Clients zum Wechsel angeboten wird noch den Knopf "Start Monitoring" drücken, das Profiling beginnt.

TPTP seit Java 5 - Test and Performance Tools Plattform


Gleiches Akronym mit etwas modifizierter Übersetzung, nun konform zur JVMTI Schnittstelle und bestens integriert in die Eclipse IDE (TPTP).

Profiling der JRockit - JRCMD


Die aktuelle Version von JRockit beinhaltet eine Tool mit dem man grundlegende JVM Eigenschaften untersuchen kann. Es eignet sich hervorragend für eine schnelle Analyse bei Laufzeitproblemen mit der JRockit JVM und man kann es intuitiv erlernen. 'jrcmd' ohne Parameter gibt alle laufenden JRockit JVM Prozesse und den Namen der Main-Klasse.

> jrcmd
> 1825 weblogic.Server
> 2334 com.util.Launcher 
Auf einen dieser Prozesse bezieht man sich nun. Aber zunächst kann man sich mit der help-Option alle Kommandos anschauen. Noch mehr Informationen gibt es mit jrcmd help all. Hier die wichtigsten Optionen:
memprof: Memory leak detection
    sampleRate         - number of seconds between samples (default: 10s)
    trendSize          - number of positive samples to consider (default: 5)
    forceThreshold     - size in bytes to force full stack stats (default: 250k) 
    verboseResultStats - print all known stats, not just leaking (default: false)
runsystemgc: java.lang.System.gc()
runfinalization: java.lang.System.runFinalization()
heap_diagnostics: heap diagnostic
print_class_summary: alle geladenen Klassen
print_object_summary: Objekte auf dem Heap
    name1, name2, name3 - Klassennamen, die gezeigt werden sollen (mit / statt .)
    increaseonly   - (bool) zeige nur Klassen die zunehmen
    largestarrays  - zeige die 10 größten Arrays vom Typ name1
    activatetrendanalysis - Trendanalysis, relativer Zuwachs nach jeder GC.
    deactivatetrendanalysis - Trendanalyse aus
print_properties -> int the Java and VM properties.
Echte Analysen brauchen auch hier Werkzeuge, die die enorme Datenflut beim Profiling visuell aufbereiten. Hier lassen sich mit
jrcmd 1234 jrarecording time=600 filename=prof.xml.zip
Profiling-Dumpfiles erzeugen und anschließend mittels eines GUI (der JRA) untersuchen. Ein Profil kann übrigens auch über Startoptionen des Servers oder über die Managementkonsole angelegt werden. Leider müssen für diese Profiling-Funktionen extra Lizenzen gekauft werden.

Manipulation von java.lang.Object


Kommt ein Einsatz professioneller Profiling-Werkzeuge nicht in Frage, kann man vielleicht auf der Sprachebene improvisieren. So ließen sich alle angelegten Objektinstanzen zählen, indem man im Konstruktor von java.lang.Object (den ja alle Konstruktoren durchlaufen) einen Zähler hochzählt und in finalize entsprechend runterzählt.

Dazu ist zunächst die Klasse java.lang.Object entsprechend zu manipulieren. Aus den Java-Quellen legt man sich eine Kopie von java.lang.Object in den Workspace und compiliert die Klasse ganz normal mit dem JDK:

public class Object {
  :
  public Object() {
    System.out.println(this.getClass().getName());
  }
  :
}
Startet man nun ein Testprogramm passiert - zunächst garnichts. Ja, richtig. Hier ist zwar eine manipulierte Version der Klasse java.lang.Object erstellt worden, allerdings nimmt die Laufzeit standardmäßig die Klassen des JRE, da diese Bestandteil des Bootclasspath sind. Es muss deshalb beim Starten der Anwendung der Bootclasspath manipuliert werden:
-Xbootclasspath/p: <Pfad zur manipulierten java.lang.Object.class>
Das Programm wird erneut gestartet und nun passiert etwas:
Error occurred during initialization of VM
java.lang.StackOverflowError
Der Grund dafür findet sich in der rekursiven Verwendung eines Konstruktors innerhalb des Konstruktors von java.lang.Object. Die Zeile System.out.println(this.getClass().getName()) erzeugt Objektinstanzen, die wiederum durch den Konstruktor von java.lang.Object laufen müssen und so fort. Eine Möglichkeit zur Behebung des Problems besteht darin, keine Konstruktoren innerhalb des manipulierten Konstruktors von java.lang.Object zu rufen. Da aus genannten Gründen auch im statischen und nichtstatischen Initialisierungsbereich von java.lang.Object keine Objektinstanzen angelegt werden dürfen, ist dieses Vorgehen nutzlos. Es muss eine andere Lösung gefunden werden.

Innerhalb eines synchronisierten Bereichs im Konstruktor von java.lang.Object wird ein Flag gesetzt, das dafür sorgt, dass während der Abarbeitung nun keine Objektinstanzen erzeugt werden. Beim Verlassen des Konstruktors wird das Flag wieder zurückgesetzt.

public class Object {
  :
  private static boolean inConstructor = true;
  public Object() {
    synchronized(Object.class) {
      if (!inConstructor) {
        inConstructor = true;
        System.out.println(this.getClass().getName());
        inConstructor = false;
      }
    }
  }
  public static void trace() {
    synchronized(Object.class) {
      inConstructor = false;
    }
  }
  :
}
  :
  public static void main(String[] args) {
    Object.trace();
    JFrame frame =  new JFrame();
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.show();
  }
  :
Bei diesem Vorgehen werden die Objektinstanzen innerhalb des Konstruktors von java.lang.Object und vor dem Aufruf von Object.trace() nicht bei der Abarbeitung erfasst. Das ist für die Zwecke des Profilings normalerweise auch nicht notwendig. Unbedingt hinzugefügt werden muss, dass derart manipulierte Laufzeiten nicht ausgeliefert werden dürfen.

Auswerten eines Heap-Abbildes: Finden von Speicherlecks


Objektinstanzen werden nicht von der GC aufgeräumt, wenn sie von einer GC Root direkt oder indirekt referenziert werden. Das sind in jedem Fall:

  • Statische Felder
  • Lokale Variablen und Methodenparameter auf einem Abarbeitungsstack. Dazu zählen aber auch jene lokalen Objekte für die es keinen syntaktischen Konstrukt gibt, um auf diese zugreifen zu können. Im Beispiel wird obj nicht von der GC erfasst, solange die Methode nicht verlassen wird, obwohl obj außerhalb des Blocks nicht mehr genutzt werden kann:
    :
    {
       Object obj = new Object();
    }
    Thread.wait(); // kann lange dauern: obj wird nicht abgeräumt
    :
    
  • Lebende Threads (also Threads im Zustand running, sleeping oder waiting)
  • Monitor Objects: Instanzen, die als Monitor für Synchronierung benutzt werden
  • JNI Local/Global: native Objekte
  • betimmte JVM interne Objekte: Beispiele sind der Systemclassloader, damit auch alle Class-Instanzen, die vom Systemclassloader geladen werden und einige Exception-Klassen
Es gibt also Instanzen im Heap-Abbild, die von der GC nie weggeräumt werden, da sie eine GC Root sind oder ohne unser Zutun von einer solchen referenziert werden. Nun kann man sich alle referenzierten Instanzen rekursiv ansehen und sich überlegen, ob man durch geschicktes Dereferenzieren überflüssige Referenzgrafen von allen GC Roots abschneiden kann. Nur für triviale Fälle kommt man so zum Erfolg, da man als Mensch das komplizierte Gefelecht der Referenzierung nicht durchschaut.

Dominator Tree Analysis


Eine Begutachtung des physikalischen Objektgraphs hilft also normalerweise nicht weiter. Was wirklich interessiert ist die Information, ob eine Instanz A der "Dominator" einer Instanz B ist. Das ist dann der Fall, wenn alle (!) Referenzpfade einer GC Root zu B durch A gehen. In einem Dominator Tree wird diese Relation als Hierarchie dargestellt. Außerdem wird der Speicher aufaddiert, der von einer Instanz und all seinen dominierten Instanzen belegt wird (das sogenannte Retain Set wird ausgewertet). Es zeigt sich nun folgendes:

  • Ursachen für Speicherlecks sind oftmals die Instanzen im Dominator Tree, die nahe einer GC Root ein großes Retain Set haben.
  • Der Dominator Tree enthält viel weniger Information als der Referenzgraf, aber exakt die Information, die normalerweise nötig, um ein Speicherleck zu identifizieren.
  • Für einen Dominator Tree wird kein "allocation tracing" gebraucht, also die Information, wann und wo Objektinstanzen erzeugt werden. Eine Dominator Tree Analyse ist deshalb auch in produktiven Umgebungen möglich.

copyright © 2002-2012 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved