Java Performance Optimization: Eight Anti-Patterns That Slow Down Your Code

✍️ OpenClawRadar📅 Published: March 20, 2026🔗 Source
Java Performance Optimization: Eight Anti-Patterns That Slow Down Your Code
Ad

Performance Improvements from Fixing Anti-Patterns

Jonathan Vogel built a Java order-processing app that initially had 1,198ms elapsed time, handled 85,000 orders per second, used just over 1GB heap, and had 19 GC pauses. After fixing eight anti-patterns without architectural changes or JDK updates, performance improved to 239ms elapsed time, 419,000 orders per second, 139MB heap, and 4 GC pauses. This represents 5x throughput, 87% less heap usage, and 79% fewer GC pauses.

Eight Java Performance Anti-Patterns to Fix

  • String concatenation in loops - O(n²) copying from immutability
  • O(n²) stream iteration inside loops - streaming the full list per element
  • String.format() in hot paths - slowest string builder, parses format every call
  • Autoboxing in hot paths - millions of throwaway wrapper objects
  • Exceptions for control flow - fillInStackTrace() walks the entire call stack
  • Too-broad synchronization - one lock becomes the bottleneck
  • Recreating reusable objects - ObjectMapper, DateTimeFormatter, Gson per call
  • Virtual thread pinning (JDK 21-23) - synchronized + blocking I/O pins carriers
Ad

Detailed Examples and Fixes

1. String Concatenation in Loops

Problem code:

String report = "";
for (String line : logLines) {
    report = report + line + "\n";
}

This creates O(n²) copying due to String immutability. BellSoft JMH benchmarks show that when n grows by 4x, loop-concatenation slows down by more than 7x.

Fix:

StringBuilder sb = new StringBuilder();
for (String line : logLines) {
    sb.append(line).append("\n");
}
String report = sb.toString();

Note: Since JDK 9, the compiler optimizes single-line concatenation like "Order: " + id + " total: " + amount, but this optimization doesn't carry into loops.

2. Accidental O(n²) with Streams Inside Loops

Problem code:

for (Order order : orders) {
    int hour = order.timestamp().atZone(ZoneId.systemDefault()).getHour();
    long countForHour = orders.stream()
        .filter(o -> o.timestamp().atZone(ZoneId.systemDefault()).getHour() == hour)
        .count();
    ordersByHour.put(hour, countForHour);
}

This pattern accounted for nearly 71% of CPU stack samples in the JFR recording. With 10,000 orders, it performs 100 million comparisons instead of a single pass.

Fix:

for (Order order : orders) {
    int hour = order.timestamp().atZone(ZoneId.systemDefault()).getHour();
    ordersByHour.merge(hour, 1L, Long::sum);
}

This provides O(n) performance with one pass. You could also use Collectors.groupingBy(... Collectors.counting()) in a single stream pipeline.

The article is part 1 of a 3-part Java Performance Optimization series, with parts 2 and 3 coming soon. Part 2 will walk through the profiling data behind these numbers, including flame graphs and which methods were actually hot.

📖 Read the full source: HN AI Agents

Ad

👀 See Also