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

OpenClaw v2026.3.22 Update Issues and 30-Second Fixes
The OpenClaw v2026.3.22 update introduced 12 breaking changes, including ClawHub becoming the default plugin store and deprecated environment variables. Five common disasters with quick fixes include API billing spikes, unintended agent actions, and configuration errors.

Practical techniques to reduce state drift in multi-step AI agents
A developer shares concrete methods to fix state drift in multi-agent workflows, including snapshot-based reads, append-only writes, and separating state from context. These approaches made runs reproducible and debugging traceable.

Claude Code Skills vs. Custom Agents: A Mental Model Based on Task Consistency
A Reddit user clarifies the distinction between Claude Code skills and custom agents: skills execute the same steps every time, while custom agents require reasoning and adaptation. The post also covers parallel subagents, delegation, hooks, and building blocks.

Canary Instance Setup for Safe OpenClaw Upgrades
A Reddit user shares a detailed canary methodology for testing OpenClaw upgrades before production: isolated config root, separate port, smoke test matrix, and a structured upgrade report format.