Benchmarking Java Code with JMH
Fast recall
Quick revision
Core answer
Whether you know that naive microbenchmarks in Java are unreliable due to JIT compilation and dead code elimination, and that JMH exists specifically to handle these pitfalls.
Writing a benchmark with System.currentTimeMillis() in a loop without warmup, which measures JIT compilation time and dead code elimination artifacts rather than steady-state performance.
Explain JVM warmup phases, dead code elimination (why Blackhole matters), and how to interpret JMH output including error margins and mode differences (Throughput vs AverageTime).
Interview-ready guide
Interview answer
JMH (Java Microbenchmark Harness) exists because the JVM's JIT compiler and dead code elimination make naive benchmarks produce results that have nothing to do with actual performance. JMH handles these pitfalls automatically.
Why naive benchmarks are wrong
- The JVM optimizes away code that doesn't affect the program's output — a loop that computes a value but never uses it can be compiled to zero instructions; your "benchmark" measures nothing
- Dead code elimination: if the benchmarked code's result is never consumed, the JIT may eliminate the computation entirely; JMH requires returning the result from the benchmark method or using a
Blackholeto consume it - JIT warmup: the first executions of a method run in the interpreter; after enough invocations, the JIT compiles it; an early benchmark captures interpreter performance, not JIT-compiled performance; JMH runs multiple warmup iterations before recording measurements
Writing a JMH benchmark
- Annotate a class with
@BenchmarkMode(Mode.AverageTime),@OutputTimeUnit(TimeUnit.MICROSECONDS),@Warmup(iterations = 5),@Measurement(iterations = 10) - Annotate the benchmark method with
@Benchmark; return the result or pass it toBlackhole.consume(result)to prevent dead code elimination @State(Scope.Benchmark)on a class holding shared state ensures thread-safe state management across benchmark threads
Running the benchmark
- Build the benchmark JAR:
mvn clean verify; run:java -jar target/benchmarks.jar -f 1 -wi 5 -i 10 -f 1: one fork (new JVM process for isolation);-wi 5: 5 warmup iterations;-i 10: 10 measurement iterations; each iteration typically runs for 1 second- Analyze the output:
ns/opis nanoseconds per operation;±is the margin of error; results with high error margins indicate interference (GC, OS scheduling) — rerun with more forks
What to benchmark
- Benchmark the hot path that you measured as a bottleneck in production (flamegraph); benchmarking cold paths wastes time; JMH results are only meaningful for the specific JDK version, hardware, and configuration tested
Benchmarking Java Code with JMH
Direct Answer
JMH handles JVM warmup and dead code elimination that make naive benchmarks unreliable; annotate with @Benchmark, run with sufficient warmup iterations, and use Blackhole to prevent DCE.
Common Mistakes & Pitfalls
Writing a benchmark with System.currentTimeMillis() in a loop without warmup, which measures JIT compilation time and dead code elimination artifacts rather than steady-state performance.
Check your understanding
Answer these without looking back at the article.
- What is dead code elimination and why does it matter for benchmarks?
- How do you benchmark code that involves I/O rather than pure computation?
- What is the difference between @State(Scope.Thread) and @State(Scope.Benchmark)?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.