Performance and Profiling
AdvancedMeasure before optimising — use JMH for micro-benchmarks, profilers for CPU/memory hotspots, and understand JIT effects on measurements.
Overview
Java performance work follows a strict cycle: measure, identify bottleneck, fix, measure again. Guessing at bottlenecks wastes time. JMH (Java Microbenchmark Harness) is the standard tool for accurate micro-benchmarks — it handles JIT warm-up, dead code elimination, and statistical analysis. Profilers (VisualVM, YourKit, async-profiler) identify CPU hotspots and allocation pressure in real workloads.
JMH Micro-benchmarks
JMH (from OpenJDK) is the only reliable way to measure Java performance at the method level. It handles: JIT warm-up iterations, dead code elimination prevention (Blackhole), fork isolation, and statistical reporting.
Never use System.currentTimeMillis() or nanoTime() directly for benchmarks — the results are unreliable without warm-up and isolation.
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, warmups = 1)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
public class StringBenchmark {
@Param({"10", "100", "1000"})
private int size;
private String[] data;
@Setup
public void setUp() {
data = IntStream.range(0, size)
.mapToObj(Integer::toString)
.toArray(String[]::new);
}
@Benchmark
public void concatenationPlus(Blackhole bh) {
String result = "";
for (String s : data) result += s;
bh.consume(result); // prevents dead code elimination
}
@Benchmark
public void stringBuilder(Blackhole bh) {
StringBuilder sb = new StringBuilder();
for (String s : data) sb.append(s);
bh.consume(sb.toString());
}
}
// Run: java -jar benchmarks.jar StringBenchmarkProfiling with async-profiler
async-profiler is a low-overhead production-safe profiler. It samples CPU stacks using OS signals (no safepoint bias — more accurate than JVMTI profilers). It can also profile allocations and lock contention.
The output is a flame graph: wide horizontal bars are hot code paths. The x-axis is time share, not time order.
# async-profiler — attach to running process
./asprof -d 30 -f /tmp/flamegraph.html <pid>
# Profile allocations
./asprof -e alloc -d 30 -f /tmp/alloc.html <pid>
# Profile lock contention
./asprof -e lock -d 30 -f /tmp/locks.html <pid>
# As a JVM agent (all modes available from startup)
java -agentpath:/path/to/libasyncProfiler.so=start,event=cpu,file=/tmp/cpu.html MyApp
# Reading flame graphs:
# • Width = total time share in that method and its callees
# • Height = call stack depth
# • Wide flat bar at the top = where CPU is spending most time
# • Narrow bars = rarely executed code
# • Look for wide bars in unexpected places (serialisation, boxing, locks)
# JFR + Mission Control (built into JDK — zero additional install)
jcmd <pid> JFR.start name=prof settings=profile duration=60s filename=/tmp/app.jfr
# Open app.jfr in JDK Mission Control for method profiling, GC analysis, etc.Common Performance Traps
The most impactful performance improvements come from algorithm changes (O(n²) → O(n log n)), data structure choices (LinkedList vs ArrayList), and eliminating unnecessary I/O — not micro-optimisations.
Common Java-specific traps: excessive autoboxing in tight loops, String concatenation in loops, regex compilation inside loops, N+1 queries in ORM, and excessive logging in hot paths.
// TRAP 1: Autoboxing in tight loop
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
counts.merge(word, 1, Integer::sum); // Integer boxing each iteration
}
// Fix: use MutableInt from Apache Commons, or LongAdder per key
// TRAP 2: Regex compiled per call
public boolean isValid(String s) {
return s.matches("\\d{4}-\\d{2}-\\d{2}"); // compiles regex every call!
}
private static final Pattern DATE = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
public boolean isValid(String s) { return DATE.matcher(s).matches(); }
// TRAP 3: Logging in hot path
for (Order order : millionOrders) {
log.debug("Processing order: {}", order); // string build even if DEBUG off?
// SLF4J {} is lazy, but isDebugEnabled() check is still good practice
if (log.isDebugEnabled()) log.debug("Processing: {}", order.getId());
}
// TRAP 4: N+1 queries
// BAD: fetches 1 user list + N order queries
List<User> users = userRepo.findAll();
users.forEach(u -> u.getOrders().size()); // lazy load per user
// GOOD: join fetch in one query
List<User> users = userRepo.findAllWithOrders(); // single JOIN queryKey Points to Remember
- Measure first — profiler before optimiser. Guessing at bottlenecks wastes time.
- JMH is the only reliable Java micro-benchmark tool — handles warm-up and dead code elimination.
- async-profiler provides accurate CPU/allocation/lock flame graphs with minimal overhead.
- Algorithm and data structure choices have far more impact than micro-optimisations.
- Common traps: autoboxing in loops, regex per call, N+1 queries, excessive logging in hot paths.
Practice Performance and Profiling in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhy is System.nanoTime() insufficient for Java benchmarking?
What is a flame graph and how do you read it?
What is safepoint bias and why does async-profiler avoid it?
How would you diagnose an N+1 query problem in a Spring application?
What JMH annotations are needed for a reliable benchmark?
Ask Aria about Performance and Profiling
Your personal AI tutor — ask anything about this concept