JVM tool guide · testing

JMH: Write Correct Java Microbenchmarks

JMH (Java Microbenchmark Harness) is the OpenJDK project's tool for writing correct microbenchmarks. Reasoning about JVM performance by hand is hopeless — the JIT compiles, inlines, and eliminated code, and naive stopwatch loops get optimized to nothing. JMH handles warm-up, dead-code elimination, black-holes, and forking so your numbers mean something.

It runs from a standalone right-jar or via Maven/Gradle plugins. The golden rules: black-hole every result, warm up genuinely, and never draw conclusions you didn't measure.

Official JMH (Java Microbenchmark Harness) project

Use it when

• You need a trustworthy number for a hot code path or an algorithm comparison.

• You're choosing between two implementations and want an apples-to-apples measurement.

• You want to regression-test performance (speed compiler optimizations into CI).

Skip it when

• You need throughput under real concurrency/load — that's a load test (Gatling/k6), not a microbenchmark.

• You just need to know which method is hot in a big app — profile with async-profiler instead.

Scaffold a benchmark

JMH best works as a separate module or even a standalone main. The maven archetype is the fastest start.

mvn archetype:generate \
  -DarchetypeGroupId=org.openjdk.jmh \
  -DarchetypeArtifactId=jmh-java-11-archetype \
  -DgroupId=com.example -DartifactId=bench

A minimal benchmark

Annotate methods with @Benchmark. Use a Blackhole to consume results so the JIT cannot void them.

@Benchmark
@BenchmarkMode(Mode.Throughput)
@Fork(2)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
public void sumLoop(Blackhole bh) {
  long acc = 0;
  for (int i = 0; i < 1000; i++) acc += i;
  bh.consume(acc);
}

Run and read

Run via the class with main (each @Benchmark is a separate measurement set). The output prints a score with the chosen Mode and unit (e.g., ops/ns for Throughput).

mvn package
java -jar target/benchmarks.jar

Blackhole — the rule you can't skip

A plain loop that returns nothing or whose result is unused is dead code the JIT removes. blackhole.consume(x) forces the result to be observed, and blackhole.consumeCPU(int) burns time to shape loop bodies without producing observable values.

Quick start

Get productive in minutes

Measure a tiny method

Write, build, run.

@Benchmark public void twice(Blackhole bh) {
  bh.consume(computeDouble());
}

java -jar target/benchmarks.jar

Frequently asked questions

Why is my naive loop showing zero time?

The JIT detected the computation has no side effects and eliminated it. That's precisely why JMH forces you to consume results with a Blackhole — otherwise the benchmark measures nothing.

What do forks, warmup, measurement mean?

Fork runs the benchmark in a fresh JVM (isolating JIT state). Warmup iterates untouched samples to let the JIT settle before timing. Measurement timing runs follow. The defaults are good; change them when you need higher variance control.

Should every project microbenchmark?

No — microbenchmarks are for hot, stable, isolated code. For whole-app performance use a profiler, then microbenchmark only the identified hotspots.

Last updated August 2026 · JVM Tools is independent and not affiliated with Oracle.