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:
- Problem klar beschreiben: Was genau ist das Symptom (CPU-Spikes, Heap wächst, Timeouts)? Seit wann tritt es auf und unter welchen Bedingungen?
- Reproduzierbares Szenario schaffen: Gleicher Use-Case, ähnliche Datenmenge und Last. Ohne Reproduzierbarkeit sind Profiling-Ergebnisse nicht vergleichbar.
- Baseline messen: Dadurch können später die Änderungen objektiv beurteilt werden.
- Hypothese ableiten und Änderungen vornehmen: Aufgrund der Messungen Hypothesen aufstellen wie „Wir haben einen Memory Leak in xyz“ und darauf basierend Codeanpassungen durchführen
- 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
- InfoQ: Unleash the Power of Open Source Java Profilers
- Bellsoft: A guide to Java profiling
- brillworks: 7 Java Profiling Tools That Will Skyrocket Your Productivity
- Medium: Profiling Java Applications: Tools and Techniques
Hinweis: Dieser Blog-Beitrag wurde mit Unterstützung von KI erstellt
