Java Profiling: Ein Weg zur messbaren Optimierung

Wenn eine Java-Anwendung langsam wird oder viel RAM braucht, helfen Vermutungen selten. Profiling beobachtet die Applikation zur Laufzeit und zeigt, welche Methoden CPU fressen, wo unnötig viele Objekte entstehen und Threads blockiert werden. Dieser Beitrag zeigt, was man in der Java Virtual Machine (JVM) typischerweise misst, vergleicht Instrumentierung und Sampling und beschreibt einen pragmatischen Ablauf für messbare Optimierungen.

Was bedeutet Profiling überhaupt

Profiling ist eine dynamische Programmanalyse: Das heisst wir möchten Aussagen über das Laufzeitverhalten von Programmen machen. Dazu messen wir direkt in der laufenden JVM, wo Ressourcen verbraucht werden. Das ist besonders wertvoll, weil sich Performance-Probleme oft nicht als einfacher Bug zeigen, welcher nur aus dem Code entsteht. Ein Performance-Problem entsteht oft aus Zusammenspiel aus Code, Daten, Garbage Collection, I/O und oder anderen Faktoren.

So misst man beim Profiling zum Beispiel:

  • CPU-Zeit: Welche Methoden und Call-Paths verbrauchen wie viel Rechenzeit?
  • Ausführungsdauer & Aufrufanzahl: Wie oft wird eine Methode aufgerufen und wie lange dauert sie?
  • Heap-Nutzung: Wie entwickelt sich der Speicherverbrauch? Welche Objekte bleiben lange im Heap?
  • Garbage Collection: Häufigkeit und Dauer von GC-Pausen
  • Threads und deren Zustand: Running/Blocked/Waiting

Ein wichtiger Grundsatz: Das Profilen sollte so nahe wie möglich am realen Verhalten gemacht werden. Idealerweise wird das Problem mit einem Lasttest oder einem klar definierten Use-Case reproduziert. Nur dann sind die Zahlen wirklich aussagekräftig.

Zwei Wege zu Messdaten: Instrumentieren und Samplen

Instrumenting Profilers

Nehmen wir an, Sie möchten herausfinden, wie lange die Ausführungszeit einer Methode ist. Ein einfacher, erster Ansatz ist es, die Methode anzupassen.

So wird aus der Methode:

void magicMethod() {
  // do some really cool stuff here
}

Beispielsweise folgende:

void magicMethod() {
  long start = System.currentTimeMillis();
  // do some really cool stuff here
  long duration = System.currentTimeMillis() - start;
  System.out.println("magicMethod took " + duration + "ms");
}

So können wir im Log nachvollziehen, wie viele Male der Code aufgerufen wurde und wie lange der einzelne Methodenaufruf gedauert hat.

Das ist stark vereinfacht das, was ein Instrumenting Profiler für uns macht: Er erweitert all unsere Methoden mittels zusätzlichen Logstatements am Anfang und Ende. Dies geschieht normalerweise über einen Java Agenten der Bytecode Modifikation zur Runtime durchführt.

Der Nachteil solcher Instrumenting Profilers liegt auf der Hand: Diese Art von Instrumentierung bringt uns nur begrenze Informationen über unseren Code und erzeugt, durch die zusätzlichen Methodenaufrufe, massiven Overhead.

Aus diesen Gründen haben Instrumenting Profilers heute keinen hohen Stellenwert mehr.

Sampling Profilers

Sampling ist im Grunde ein immer wiederkehrendes statisches Messen. Dabei wird nicht jeder Methodenaufruf angeschaut, sondern es werden in regelmässigen Abständen (typischerweise alle 10–20 ms) Momentaufnahmen der laufenden Anwendung gemacht.

Dabei werden, um leichtgewichtig zu bleiben, pro Intervall oft nicht alle Threads gesampelt, sondern nur eine kleine, zufällige Auswahl (z.B. 5–8 Threads). Innerhalb eines Intervalls sendet der Profiler dem ausgewählten Thread ein Signal, welcher diesen anhält und seinen Stacktrace notiert. So wird fortlaufend ein Bild über die Applikation erstellt.

Der Vorteil bei dieser Methode ist, dass ein geringer Overhead generiert wird. Jedoch muss man beachten, dass kurz laufende Methoden, die zwischen den Abständen der Messungen ausgeführt werden, zeitlich verpasst werden können.

Ein pragmatischer Ablauf für die Praxis

Falls ein Problem festgestellt wird, kann sich an folgendem Ablauf festhalten:

  1. Problem klar beschreiben: Was genau ist das Symptom (CPU-Spikes, Heap wächst, Timeouts)? Seit wann tritt es auf und unter welchen Bedingungen?
  2. Reproduzierbares Szenario schaffen: Gleicher Use-Case, ähnliche Datenmenge und Last. Ohne Reproduzierbarkeit sind Profiling-Ergebnisse nicht vergleichbar.
  3. Baseline messen: Dadurch können später die Änderungen objektiv beurteilt werden.
  4. Hypothese ableiten und Änderungen vornehmen: Aufgrund der Messungen Hypothesen aufstellen wie „Wir haben einen Memory Leak in xyz“ und darauf basierend Codeanpassungen durchführen
  5. Nachmessen & vergleichen: Gleiche Bedingungen wie bei der Baseline. Nur so ist man sich sicher, dass die Optimierung wirklich wirkt.

Tools

Nachfolgend eine Auflistung mir bekannter Tools zu diesem Thema:

Fazit

Profiling macht Vermutungen messbar. Es macht sichtbar, wo Zeit, Speicher und Threads tatsächlich verloren gehen. Wer strukturiert vorgeht, führt belegbare Änderungen ein und entwickelt so nachhaltige Software die performt.

Quellen & weiterführende Links

Hinweis: Dieser Blog-Beitrag wurde mit Unterstützung von KI erstellt

Beitrag teilen

Romeo Köppel

Romeo Köppel ist Software Engineer bei der MediData AG und bloggt aus dem Unterricht des CAS Modern Software Engineering & Development.

Alle Beiträge ansehen von Romeo Köppel →

Schreibe einen Kommentar