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

AGENTS.md Done Right: A 25% Correctness Boost — or a 30% Drop
Augment Code tested AGENTS.md files head-to-head: the best ones rival a model upgrade from Haiku to Opus; the worst ones hurt output. Decision tables, procedural workflows, and progressive disclosure win.

Building a serverless AI agent platform on AWS for $0.01/month with Claude Code
A developer built a complete AWS serverless platform running AI agents for approximately $0.01/month using Claude Code over 29 hours, eliminating expensive components like NAT Gateway ($32/month) and ALB ($18/month). The project includes 233 unit tests, 35 E2E tests, and deploys with a single cdk deploy command.

Mastering Backup: Safeguarding Your OpenClaw Agent
In an era dominated by automation and AI, ensuring the safety of your OpenClaw agent through robust backup strategies is paramount. Learn the essential steps to secure your digital assistant.

Local LLM Setup Recommendations for OpenClaw
A user shares their configuration for running a local LLM with OpenClaw, using a GB10 for AI processing and a Mac mini for the OpenClaw installation, with specific model and server details.