W3docs

Java Performance-Tipps

Praktische Performance-Tipps für Java: zuerst messen, Premature Optimization vermeiden, häufige Mikro-Optimierungen kennenlernen.

Java ist für fast alles schnell genug, aber langsamer Code kommt dennoch vor — meist durch zu viel Arbeit, zu viele Speicherzuweisungen oder die falsche Datenstruktur. Der wichtigste Performance-Tipp ist gar kein Trick: Messen Sie, bevor Sie irgendetwas ändern. Dieses Kapitel zeigt, wie man ehrlich misst, welche Optimierungen am häufigsten Früchte tragen (String-Aufbau, Wahl der Datenstruktur, unnötige Allokationen vermeiden) und welche Fallstricke naiv „schnellen" Code tatsächlich langsam machen.

Erst messen, dann optimieren

Geraten zu raten, wo ein Performance-Problem liegt, bedeutet, Stunden damit zu verbringen, Code zu beschleunigen, der nie langsam war. Profilieren Sie eine echte Arbeitslast, finden Sie den Hot Path (den kleinen Codeanteil, in dem die meiste Zeit verbracht wird), und optimieren Sie erst dann. Für schnelle Experimente liefert System.nanoTime() ein Wall-Clock-Delta; für ernsthafte Benchmarks verwenden Sie ein Tool wie JMH (Java Microbenchmark Harness), das den JIT aufwärmt und Messrauschen berücksichtigt.

long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");

Zwei Regeln gehören dazu. Erstens: Premature Optimization vermeiden — klarer Code, der schnell genug ist, schlägt cleveren Code, der schwer lesbar ist. Zweitens: Der JIT-Compiler der JVM optimiert häufig ausgeführten Code zur Laufzeit; eine Methode erreicht erst volle Geschwindigkeit, nachdem sie viele Male gelaufen ist — ein einzelner Zeitmesslauf sagt wenig aus.

Strings mit StringBuilder aufbauen

Da Strings unveränderlich sind, erzeugt s += x in einer Schleife bei jeder Iteration einen brandneuen String und kopiert alle bisherigen Zeichen — das ist O(n²)-Aufwand. StringBuilder hält einen wachsenden Puffer vor und hängt an Ort und Stelle an, wodurch dieselbe Aufgabe zu O(n) wird.

// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
    csv += field + ",";
}

// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
    sb.append(field).append(',');
}
String csv2 = sb.toString();

Ein einzelnes a + b + c außerhalb einer Schleife ist in Ordnung — der Compiler wandelt es bereits in einen StringBuilder um (siehe String-Verkettung für das Verhalten des Compilers). Das Problem entsteht bei der Verkettung innerhalb einer Schleife, wo jeder Durchlauf eine weitere vollständige Kopie erzeugt.

Die richtige Datenstruktur wählen

Die größten Gewinne kommen meist aus algorithmischen Entscheidungen, nicht aus Mikro-Tweaks. In einer ArrayList nach einem Wert zu suchen, durchläuft jedes Element (O(n)); eine HashMap oder HashSet erledigt das in annähernd konstanter Zeit (O(1)). Wählen Sie die Collection, die dem tatsächlichen Zugriffsmuster entspricht.

BedarfVerwendeZugriffskosten
Indexzugriff, Anhängen am EndeArrayListO(1) per Index, O(n) per Wert
Schlüssel/Wert-SucheHashMapO(1) durchschnittlich
Zugehörigkeitstest, keine DuplikateHashSetO(1) durchschnittlich
Sortierte SchlüsselTreeMapO(log n)
Häufiges Einfügen/Entfernen an den EndenArrayDequeO(1) an den Enden

Wenn Sie die endgültige Größe kennen, übergeben Sie sie dem Konstruktor: new ArrayList<>(10_000) oder new HashMap<>(capacity). Damit werden die wiederholten Neuzuweisungen und Kopiervorgänge vermieden, die beim Wachsen einer Collection entstehen.

Unnötige Objekterzeugung vermeiden

Jedes erzeugte Objekt muss später eingesammelt werden, und Garbage Collection ist nicht kostenlos. Nutzen Sie unveränderliche Werte wieder, bevorzugen Sie Primitive gegenüber ihren Box-Typen in engen Schleifen, und erzeugen Sie keine Objekte, die Sie sofort wieder wegwerfen.

// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i;   // slow, allocates boxes

// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i;     // fast

Weitere einfache Gewinne: Kompilierte Pattern-Objekte cachen statt String.matches() in einer Schleife aufzurufen, einen DateTimeFormatter wiederverwenden (er ist thread-sicher und unveränderlich), und in den heißesten inneren Schleifen, in denen Allokationen eine Rolle spielen, das erweiterte for gegenüber Streams bevorzugen.

java— editable, runs on the server

Was man aus der Ausführung mitnimmt:

  • Same result? true beweist, dass StringBuilder exakt denselben String wie += erzeugt — der Austausch ändert nur die Geschwindigkeit, nie die Korrektheit.
  • Das ausgegebene „x-mal langsamer"-Verhältnis zeigt, dass Schleifenverkettung um ein Vielfaches teurer ist als das Anhängen an einen Puffer, weil jedes += den gesamten bisherigen String kopiert.
  • Both lists size 100000: true bestätigt, dass eine vorab dimensionierte ArrayList mit einer gewachsenen identisch ist — der Konstruktor-Hinweis beeinflusst die Allokation, nicht den Inhalt.
  • Pre-sized faster? true zeigt, dass das Mitteilen der Kapazität an ArrayList im Voraus die wiederholten Resize-und-Kopier-Schritte vermeidet.
  • Map lookups found: 50000 in ... ms demonstriert, dass 50.000 HashMap-Suchen in etwa einer Millisekunde abgeschlossen sind — der Gewinn aus O(1)-Zugriff statt O(n)-Listen-Scan.

Häufige Fallstricke

Einige Fehler machen „offensichtlich schnelleren" Code in sein Gegenteil:

  • Einem einzelnen Zeitmesslauf vertrauen. Der JIT ist noch nicht aufgewärmt, und das OS hat möglicherweise mitten in der Messung etwas anderes eingeplant. Wiederholen Sie die Arbeit tausende Male oder verwenden Sie JMH, bevor Sie einer Zahl vertrauen.
  • Kalten Code mikro-optimieren. Eine Methode, die beim Start einmal läuft, gewinnt nichts durch eine engere Schleife. Investieren Sie Aufwand nur in den Hot Path, auf den der Profiler zeigt.
  • Strings mit + in einer Schleife aufbauen. Die mit Abstand häufigste vermeidbare Verlangsamung — greifen Sie immer zu StringBuilder, wenn Sie innerhalb einer Schleife verketten.
  • Verstecktes Autoboxing. Eine List<Integer>, Map<Integer, Integer> oder ein unbedachter Long-Akkumulator boxt jeden Wert. In einer engen numerischen Schleife sollten Sie Primitive und Primitive-Arrays bevorzugen.
  • Optimieren, bevor es funktioniert. Erst korrekt, dann schnell. Klarer Code, den man profilieren kann, schlägt cleveren Code, über den man nicht nachdenken kann.

Zusammenfassung

  • Messen, bevor Sie irgendetwas ändern — profilieren Sie eine echte Arbeitslast und optimieren Sie nur den Hot Path.
  • Strings mit StringBuilder aufbauen, nicht mit +=, innerhalb von Schleifen.
  • Die Collection dem Zugriffsmuster anpassen: HashMap/HashSet für Suchen, ArrayList für indizierten Zugriff; vorab dimensionieren, wenn die Anzahl bekannt ist.
  • Unnötige Allokationen vermeiden: Primitive bevorzugen, unveränderliche Objekte wiederverwenden, und bedenken, dass jedes Objekt Garbage-Collection-Aufwand hinzufügt.

Übungen

Übung
Warum ist die Verwendung von '+=' zum Aufbauen eines Strings in einer Schleife langsamer als StringBuilder?
Warum ist die Verwendung von '+=' zum Aufbauen eines Strings in einer Schleife langsamer als StringBuilder?
Was this page helpful?