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

Evaluating Agent Skill Safety: Key Considerations Before Installation
Installing new agent skills can enhance functionality but also comes with risks. Learn how to evaluate the safety of these skills to protect your system.

5 Coherence Checks Before Any OpenClaw Profile Goes Live
Stop chasing perfect spoofing. Internal signal coherence matters more. TLS, locale, WebGL, canvas, and behavior checks from r/openclaw.

Four Common Setup Mistakes That Make People Quit OpenClaw
A Reddit user reports seeing over 50 people quit OpenClaw due to four specific setup issues: missing SOUL.md files, excessive API costs from using Opus model for everything, installing too many skills at once, and creating multiple agents before the first one works properly.

OpenClaw Mega Cheatsheet: Your Gateway to AI Coding Mastery
Dive into the OpenClaw Mega Cheatsheet from r/openclaw—a comprehensive guide packed with essential tips for AI coding and automation enthusiasts.