ODNYX.

Blog · Java

Java Performance Optimization Techniques That Actually Matter

2 min read
#java#performance#jvm

“Optimize your Java code” is often vague advice. Here are specific, concrete techniques that tend to have real, measurable impact — along with the reasoning for why each one matters, so you can judge when it actually applies to your situation.

Profile before you optimize

Tip: never optimize based on a guess. A profiler (like async-profiler, VisualVM, or your APM tool) will often surprise you about where time is actually being spent.

Developers frequently optimize the wrong thing because it “feels slow” rather than measuring where time actually goes. A 2-hour profiling session before touching code usually saves far more time than it costs.

String concatenation in loops

// Slow: creates a new String object on every iteration
String result = "";
for (String item : items) {
    result += item + ", ";
}

Each += on a String creates an entirely new object, because String is immutable in Java. In a loop, this becomes quadratic behavior as the string grows.

// Fast: StringBuilder mutates one buffer instead of allocating repeatedly
StringBuilder result = new StringBuilder();
for (String item : items) {
    result.append(item).append(", ");
}

StringBuilder avoids the repeated allocation entirely, making this linear instead of quadratic for large inputs.

Choosing the right collection

Using the wrong collection type for the access pattern is a common, easy-to-fix bottleneck:

Access pattern Use
Frequent lookups by key HashMap
Frequent lookups by key, need insertion order LinkedHashMap
Frequent contains()/add() on a set HashSet
Frequent index-based access ArrayList
Frequent insertion/removal at both ends ArrayDeque

A common real mistake: using ArrayList.contains() in a hot loop, which is O(n) per call, when a HashSet would make the same check O(1).

JVM heap sizing

Setting -Xmx (max heap) too small causes excessive, expensive garbage collection cycles; setting it far larger than needed can increase GC pause times when collections do happen (more memory to scan). A reasonable starting point for many workloads is sizing the heap to comfortably fit your application’s actual working set, then tuning based on GC logs (-Xlog:gc) rather than guessing.

java -Xms512m -Xmx512m -Xlog:gc -jar app.jar

Setting -Xms (initial heap) equal to -Xmx avoids the overhead of the heap resizing itself during warmup.

Avoid unnecessary object creation in hot paths

Autoboxing (converting int to Integer, etc.) inside tight loops creates unnecessary object churn. Where performance matters in a hot path, prefer primitive arrays and primitive-typed collections over their boxed equivalents.

What’s next

Performance work pays off most when it’s guided by actual measurement — pick the technique that matches a bottleneck you’ve confirmed, not one you’re guessing about.