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

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
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
👀 See Also

Practical workflow patterns for reliable AI coding in multi-file projects
A Reddit user shares four specific workflow improvements that increased reliability for AI coding on multi-file projects: spec-first starts, task decomposition with checkpoints, stable operating loops, and signal-only review.

The LLM Voice Problem: Avoiding AI-Generated Writing Patterns
A developer discusses the common issue of LLM-assisted writing having recognizable "LLM-isms" that trigger immediate AI detection, and shares an article on identifying these patterns and editing for authenticity.

Three Essential OpenClaw Skills for a Stable Setup: Memory, Security, and Discovery
A Reddit post recommends installing three specific types of OpenClaw skills first: a memory fix skill to prevent context loss, a local security vetting skill to check for malicious code, and a curated discovery hub to find maintained tools.

OpenClaw Installation on MacBook Pro Using Local Homebrew and NVM
A user successfully installed OpenClaw on a MacBook Pro using a non-admin account with local Homebrew, NVM v0.40.4, Python 3.14.3 via pyenv, Node 24, and the Qwen3.5-122B-A10B-MLX-vision-4.7-bit LLM through oMLX.