CodeGym /Kurse /JAVA 25 SELF /Profiling und Code-Optimierung: Werkzeuge, Ansätze

Profiling und Code-Optimierung: Werkzeuge, Ansätze

JAVA 25 SELF
Level 63, Lektion 4
Verfügbar

1. Einführung in das Profiling

Profiling ist wie eine medizinische Untersuchung für Ihr Programm: Wir schauen nicht nur auf die „Temperatur“ (Monitoring), sondern suchen, wo die Anwendung „Schmerzen“ hat, was langsam läuft und wo zu viel Speicher oder Ressourcen verbraucht werden.

Profiling ist der Prozess des Sammelns und Analysierens von Informationen über die Programmausführung, um Engpässe (bottlenecks) und ineffiziente Codestellen aufzudecken. Anders als Monitoring, das normalerweise allgemeine Kennzahlen verfolgt (CPU-Last, Speicher, Anzahl der Threads), erlaubt Profiling den Blick ins Innere: Welche Methoden werden am häufigsten aufgerufen? Wie viel Zeit benötigen sie? Wie viele Objekte werden erzeugt und wo genau tritt ein Memory Leak auf?

Wann ist Profiling wirklich nötig?

  • Die Anwendung ist „langsam“, aber es ist unklar, warum.
  • Der Speicherverbrauch ist plötzlich gestiegen.
  • Nach einem Code-Update dauert etwas länger.
  • Es muss verstanden werden, warum der Server nicht genug Ressourcen hat.

Übrigens hat fast jeder Entwickler mindestens einmal im Leben die falsche Codestelle optimiert. Warum? Weil es „mit bloßem Auge“ praktisch unmöglich ist, das Nadelöhr zu bestimmen – dafür braucht man einen Profiler.

Wichtige Profiling-Metriken

  • Methodenlaufzeit (CPU-Profiling): Welche Methoden benötigen am meisten Zeit? Wo „verbraucht“ das Programm CPU?
  • Speichernutzung (Memory-Profiling): Welche Objekte werden am häufigsten erzeugt? Wo bleiben sie länger im Speicher als nötig?
  • Anzahl der Objekte: Erzeugen wir zu viele gleichartige Objekte?
  • Threads: Gibt es zu viele Threads? Gibt es Deadlocks oder Contention?
  • Methodenaufrufe: Wie tief ist der Stack? Gibt es unendliche Rekursion?

2. Profiling-Tools

In der Java-Welt gibt es mehrere klassische (und kostenlose!) Tools, mit denen man Profiling durchführen kann. Schauen wir uns die wichtigsten an.

VisualVM

VisualVM ist ein kostenloses Tool, das im JDK enthalten ist (seit JDK 6). Es erlaubt:

  • Verbindungen zu lokalen und entfernten JVMs.
  • Ansicht von Speicher, Threads, CPU, Garbage Collection.
  • Erstellen und Analysieren eines Heap-Dumps.
  • CPU- und Speicherprofiling der Anwendung.

Wie startet man VisualVM?
Normalerweise befindet es sich im JDK-Ordner: <JDK-Pfad>/bin/jvisualvm
Starten, den Prozess der Java-Anwendung auswählen – und Sie können ihr Leben beobachten wie bei Fischen im Aquarium (nur dass die „Fische“ hier Objekte und Threads sind).

JProfiler, YourKit

Das sind kommerzielle, aber sehr leistungsfähige Tools. Sie ermöglichen:

  • Profiling von Speicher, CPU, Threads.
  • Analyse von „Snapshots“ des Speichers (Heap-Dump).
  • Suche nach Leaks, langen Sperren, langsamen Methoden.
  • Integration mit IDE und CI/CD.

Für den Anfang reicht VisualVM, aber wenn Sie zu größeren Projekten „heranwachsen“ – schauen Sie sich diese Tools an.

Java Flight Recorder (JFR)

JFR ist ein in das JDK integriertes Werkzeug zum Sammeln von JVM-Ereignissen. Es ist sehr leichtgewichtig, beeinflusst die Performance kaum und erlaubt das Sammeln von Informationen über:

  • Methodenlaufzeiten.
  • Garbage Collection.
  • Threads, Sperren, Fehler.

JFR eignet sich hervorragend für den Produktivbetrieb, wenn die Anwendung nicht ausgebremst werden darf.

3. Praxis: Wir profilieren eine einfache Anwendung

Lassen Sie uns einen Mini-Rechner erstellen, der lange Berechnungen ausführen und die Historie der Operationen speichern kann (damit wir Schleifen, Collections und Speicherarbeit haben).

Codebeispiel: „Langsamer Rechner“

import java.util.ArrayList;
import java.util.List;

public class SlowCalculator {
    private final List<String> history = new ArrayList<>();

    public int add(int a, int b) {
        simulateHeavyOperation();
        int result = a + b;
        history.add(a + " + " + b + " = " + result);
        return result;
    }

    public int multiply(int a, int b) {
        simulateHeavyOperation();
        int result = a * b;
        history.add(a + " * " + b + " = " + result);
        return result;
    }

    public List<String> getHistory() {
        return history;
    }

    // Simulation einer "aufwendigen" Operation
    private void simulateHeavyOperation() {
        for (int i = 0; i < 5_000_000; i++) {
            Math.sqrt(i);
        }
    }
}

Und nun – die Hauptklasse:

public class Main {
    public static void main(String[] args) {
        SlowCalculator calc = new SlowCalculator();
        for (int i = 0; i < 10; i++) {
            calc.add(i, i * 2);
            calc.multiply(i, i + 5);
        }
        System.out.println("Verlauf der Operationen:");
        for (String entry : calc.getHistory()) {
            System.out.println(entry);
        }
    }
}

Wie profiliert man diese Anwendung?

  1. Kompilieren und starten Sie die Anwendung.
  2. Öffnen Sie VisualVM (jvisualvm).
  3. Finden Sie Ihren Prozess (normalerweise über den Klassen­namen Main).
  4. Wechseln Sie auf die Registerkarte CPU Profiler und klicken Sie auf Start.
  5. Lassen Sie das Programm arbeiten (oder klicken Sie auf die langsame Schaltfläche noch einmal).
  6. Schauen Sie, welche Methoden am meisten Zeit benötigen.

Frage: Welche Methode ist Ihrer Meinung nach die „schwerste“?
Antwort: Natürlich simulateHeavyOperation() – denn sie dreht eine riesige Schleife mit 5_000_000 Iterationen und ruft Math.sqrt auf.

4. Typische Performance-Probleme

Langsame Algorithmen

Der häufigste Grund: unglückliche Wahl von Algorithmus oder Datenstruktur. Zum Beispiel Suche in einer Liste statt HashMap zu verwenden oder Bubblesort statt Quicksort.

Beispiel:

// Langsame Suche
for (String s : list) {
    if (s.equals("target")) {
        // gefunden
    }
}

Verwenden Sie besser Set oder Map für schnelle Suchen.

Memory Leaks

Ein Memory Leak ist eine Situation, in der Objekte „lebendig“ bleiben (es gibt Referenzen auf sie), obwohl sie nicht mehr benötigt werden. Das führt zu wachsendem Speicherverbrauch und schließlich zu OutOfMemoryError.

public class MemoryLeakDemo {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leakyList.add(new byte[1_000_000]); // 1 MB
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        }
    }
}

Wie findet man Leaks?
Erstellen Sie einen heap dump in VisualVM und schauen Sie, welche Objekte am meisten Speicher belegen und warum es Referenzen auf sie gibt.

Übermäßige Objekterzeugung

Wenn Sie in einer Schleife viele gleichartige Objekte erzeugen, belastet das nicht nur den Garbage Collector, sondern kann die Anwendung auch ausbremsen.

for (int i = 0; i < 1_000_000; i++) {
    String s = new String("hello"); // schlecht!
}

Verwenden Sie besser Konstanten oder den String-Pool (String pool).

Thread-Sperren

Wenn mehrere Threads um dieselbe Ressource konkurrieren (z. B. eine synchronisierte Methode), kann das zu Sperren und Performance-Einbrüchen führen.

public synchronized void doWork() {
    // ...
}

Wie findet man sie?
Auf der Registerkarte Threads in VisualVM sehen Sie, welche Threads „hängen“ und warum.

5. Ansätze zur Optimierung

Erst messen, dann optimieren

Die wichtigste Regel der Optimierung: Optimiere nichts, was nicht bremst.

Profilieren Sie zuerst, finden Sie die „Hotspots“ und ändern Sie erst dann den Code. Manchmal macht der vermeintlich „offensichtliche“ Codeteil nur 1 % der Laufzeit aus, während der wahre „Monster“-Verbrauch irgendwo in einer Bibliothek oder an unerwarteter Stelle steckt.

Profiler zur Suche nach Hotspots einsetzen

Ein Hotspot ist eine Methode oder Codestelle, die den größten Anteil der Laufzeit der Anwendung beansprucht.

In VisualVM ist das auf der Registerkarte CPU Profiler sichtbar:

  • Sortieren Sie Methoden nach Ausführungszeit.
  • Schauen Sie sich den Stack Trace an: Wer ruft wen auf?
  • Denken Sie daran, dass manchmal nicht Ihr Code „schuld“ ist, sondern eine Bibliothek oder sogar das JDK.

Optimierungsbeispiele

Beispiel 1: Algorithmus ersetzen
Wenn Sie feststellen, dass die meiste Zeit für Suchen in einer Liste draufgeht, ersetzen Sie List durch HashSet.

Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
    // schnell!
}

Beispiel 2: Anzahl der Allokationen reduzieren
Anstatt in einer Schleife neue Objekte zu erzeugen, nutzen Sie Wiederverwendung oder StringBuilder.

// Schlecht:
for (int i = 0; i < 10000; i++) {
    String s = "Ergebnis: " + i;
}

// Besser:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("Ergebnis: ").append(i);
    String s = sb.toString();
}

Beispiel 3: Caching
Wenn Sie sehen, dass eine aufwendige Methode oft mit denselben Parametern aufgerufen wird, verwenden Sie einen Cache.

Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
    return sqrtCache.computeIfAbsent(x, Math::sqrt);
}

6. Demonstration: Wir beschleunigen unseren Rechner

Problem: simulateHeavyOperation() benötigt viel Zeit

Schritt 1. Profiling
In VisualVM ist zu sehen, dass fast die gesamte Zeit für Math.sqrt(i) innerhalb der Schleife mit 5_000_000 Iterationen draufgeht.

Schritt 2. Optimieren
Wenn das nur Lastsimulation ist – entfernen Sie sie oder reduzieren Sie die Anzahl der Iterationen.
Wenn es echte Business-Logik ist – überlegen Sie, ob man:

  • das Ergebnis cachen kann,
  • einen schnelleren Algorithmus verwenden kann,
  • die Berechnungen in einen separaten Thread verlagern kann (falls es für den Benutzer nicht kritisch ist).

Optimierungsbeispiel:

private void simulateHeavyOperation() {
    // Vorher 5_000_000, jetzt 100_000
    for (int i = 0; i < 100_000; i++) {
        Math.sqrt(i);
    }
}

Schritt 3. Ergebnis prüfen
Profiling erneut starten – das Programm läuft schneller, die CPU-Last ist gesunken.

7. Visualisierung: Optimierungsprozess

flowchart TD
    A[Start der Anwendung]
    B["Profiling (VisualVM)"]
    C[Engpässe identifizieren]
    D[Code-Optimierung]
    E[Erneutes Profiling]
    F[Leistungsverbesserung]

    A --> B --> C --> D --> E --> F
    E --> C

8. Typische Fehler bei Profiling und Optimierung

Fehler Nr. 1: Optimierung nach Bauchgefühl. Sehr oft ändern Entwickler Code, ohne gemessen zu haben, wo das Problem wirklich liegt. Ergebnis – viel Arbeit, minimaler Nutzen.

Fehler Nr. 2: Profiling unter „unrealistischen“ Bedingungen. Profilieren Sie mit Daten und Last, die dem Echtbetrieb nahekommen. Profiling „im luftleeren Raum“ deckt die echten Probleme eventuell nicht auf.

Fehler Nr. 3: Memory Leaks ignorieren. Wenn man nicht auf den heap dump schaut und Referenzen analysiert, kann man lange übersehen, dass das Programm „aufbläht“ und bald abstürzt.

Fehler Nr. 4: Jagd nach mikroskopisch kleinen Optimierungen. Verschwenden Sie keine Tage, um Code zu beschleunigen, der nur 0,1 % der Laufzeit ausmacht. Zuerst – die wichtigsten Engpässe.

Fehler Nr. 5: Threads und Synchronisation nicht berücksichtigt. In Multithread-Anwendungen hängen Performance-Probleme oft nicht mit Algorithmen zusammen, sondern mit Sperren und Wartezeiten (synchronized, Contention).

Fehler Nr. 6: Profiling nach Änderungen vergessen. Überprüfen Sie das Ergebnis nach der Optimierung unbedingt erneut: Manchmal kann eine „Optimierung“ die Performance sogar verschlechtern!

1
Aufgabe
JAVA 25 SELF, Level 63, Lektion 4
Gesperrt
Öko-Aktivist
Öko-Aktivist
1
Aufgabe
JAVA 25 SELF, Level 63, Lektion 4
Gesperrt
Kapitän Kirk
Kapitän Kirk
1
Umfrage/Quiz
Logging, Level 63, Lektion 4
Nicht verfügbar
Logging
Logging, Monitoring und Profiling
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION