Definition
JMH is the OpenJDK's official harness for writing trustworthy JVM microbenchmarks. It is built by the same people who build the JVM, and it exists because naively timing code with System.nanoTime() gives wrong numbers. JMH automatically handles JIT warm-up, runs in forked JVMs for isolation, measures across many iterations, and provides a Blackhole to consume results so the JIT cannot delete your code as dead. You annotate methods with @Benchmark and configure the run with annotations like @State, @Setup, @Param, @Fork, @Warmup, @Measurement, and @BenchmarkMode.
Understanding the Concepts
What is JMH?
JMH is the OpenJDK's official harness for writing trustworthy JVM microbenchmarks. It is built by the same people who build the JVM. You write a benchmark by annotating a method with @Benchmark. JMH will call that method repeatedly and measure it.
How do you write a JMH benchmark?
package com.bank.benchmark;
import org.openjdk.jmh.annotations.*;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // measure average time per operation
@OutputTimeUnit(TimeUnit.NANOSECONDS) // report in nanoseconds
@Fork(2) // run in 2 separate JVMs
@Warmup(iterations = 5, time = 1) // 5 warm-up iterations (not measured)
@Measurement(iterations = 10, time = 1) // 10 measured iterations
@State(Scope.Benchmark) // shared state across threads
public class InterestBenchmark {
private BigDecimal balance;
private BigDecimal rate;
@Setup
public void setUp() {
balance = new BigDecimal("500000.00");
rate = new BigDecimal("0.04");
}
@Benchmark
public BigDecimal calculateInterest() {
// Return the result so JMH consumes it (prevents dead-code elimination)
return balance.multiply(rate);
}
}
What do the key annotations do?
| Annotation | Purpose |
|---|---|
| @Benchmark | Marks the method to be measured |
| @BenchmarkMode | Throughput, AverageTime, SampleTime, or SingleShotTime |
| @State | Holds benchmark fixtures; Scope.Benchmark for shared, Scope.Thread for per-thread |
| @Setup / @TearDown | Prepare and clean up state outside the timed region |
| @Param | Run the same benchmark across several input values |
| @Fork | Number of fresh JVMs to run (isolates from other code) |
| @Warmup | Warm-up iterations so JIT compiles hot paths before measuring |
| @Measurement | The actual timed iterations |
Why must you consume computed values?
The JIT is aggressive: if you compute a value and never use it, the JIT proves the computation has no effect and deletes it entirely (dead-code elimination). Then your benchmark measures nothing and reports absurdly fast times.
// BAD: JIT eliminates the dead computation
@Benchmark
public void badBenchmark() {
balance.multiply(rate); // result not used → JIT removes this line
}
// GOOD option 1: return the value (JMH consumes returned values)
@Benchmark
public BigDecimal goodBenchmark() {
return balance.multiply(rate);
}
// GOOD option 2: use Blackhole
@Benchmark
public void goodBenchmarkWithBlackhole(Blackhole bh) {
bh.consume(balance.multiply(rate)); // forces JVM to treat value as used
}
Why is System.nanoTime() in a loop not a valid benchmark?
A hand-rolled nanoTime loop has five fatal flaws:
- No warm-up: it times interpreted or C1-compiled code, not the C2-optimized steady state.
- No fork isolation: other code in the same JVM pollutes the measurement.
- GC noise: a GC pause during the measurement skews the result with no statistical treatment.
- Dead-code elimination: without a Blackhole, the JIT may delete the computation entirely.
- Constant folding: if the inputs are constants, the JIT may compute the result at compile time.
// This is NOT a valid benchmark
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
balance.multiply(rate); // JIT may eliminate this entirely
}
long elapsed = System.nanoTime() - start;
System.out.println(elapsed / 1_000_000 + " ns/op"); // meaningless number
How do you use @Param for parameterized benchmarks?
@State(Scope.Benchmark)
public class StatementBenchmark {
@Param({"10", "100", "1000", "10000"})
private int lineCount; // JMH runs each value
@Benchmark
public String buildStatementConcat() {
String result = "";
for (int i = 0; i < lineCount; i++) {
result += "TXN" + i + ": Rs.1000\n"; // string concatenation
}
return result;
}
@Benchmark
public String buildStatementBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lineCount; i++) {
sb.append("TXN").append(i).append(": Rs.1000\n");
}
return sb.toString();
}
}
JMH runs every combination of parameters, so you see exactly how performance scales. In banking at National Bank of India, Rajesh used this approach to prove that StringBuilder was 40x faster than concatenation for 1000-line bank statements, and wired the benchmark into the CI pipeline to catch future performance regressions.
Key Takeaway
JMH, the Java Microbenchmark Harness, is the OpenJDK's official tool for writing trustworthy microbenchmarks, and it exists because benchmarking the JVM by hand almost always produces wrong numbers. You write a benchmark by annotating a method with at-Benchmark, and you configure the run with annotations: at-BenchmarkMode picks whether you measure throughput or average time, at-State and at-Setup provide and prepare fixtures outside the timed region, at-Param sweeps the benchmark across several input values, at-Fork runs each benchmark in a fresh isolated JVM, at-Warmup runs unmeasured iterations so the JIT has time to compile the hot path to optimized native code, and at-Measurement is the actual timed phase. JMH then aggregates across forks and iterations and reports an average with an error margin so you can tell whether a difference is real.
The single most important detail is consuming your results. The JIT is aggressive, and if you compute a value and never use it, the JIT proves the computation has no observable effect and deletes it entirely, which is dead-code elimination. Then your benchmark is timing nothing and reports impossibly fast numbers. JMH solves this by consuming any value you return from the benchmark method, or by giving you a Blackhole whose consume method forces the JVM to treat a value as genuinely used. This is exactly why a hand-written loop around System.nanoTime is not a valid benchmark: it has no warm-up so it times cold interpreted code, no fork isolation so other code pollutes the measurement, no statistical treatment of GC pauses and CPU scheduling noise, and it is wide open to dead-code elimination and constant folding, so the number it prints is essentially meaningless.
In my banking work at National Bank of India, this came up when the team debated whether string concatenation was fast enough for building statement lines, and a colleague "proved" it was by timing a nanoTime loop whose result the JIT had quietly eliminated. I rewrote it as a proper JMH benchmark comparing concatenation against StringBuilder, parameterized by the number of statement lines, with warm-up, two forks, and a Blackhole consuming the results. The data was unambiguous: at a thousand lines StringBuilder was roughly forty times faster, because concatenation builds a new string every iteration. We switched to StringBuilder and wired the benchmark into our pipeline to catch future performance regressions on hot methods. The lesson I always repeat is that you never trust a benchmark that lacks warm-up, forking, and a Blackhole, because the JVM is far too clever to be measured naively.
---
## CHAPTER 27 – JVM INTERNALS & GC – COMPLETE! ALL 15 QUESTIONS ANSWERED!
Remember one sentence: What is JMH (Java Microbenchmark Harness): the standard tool for writing reliable JVM microbenchmarks that accounts for JIT warm-up, dead-code elimination, GC, and CPU scheduling: how do you set up a JMH benchmark (@Benchmark, @State, @Setup, @Param, @Fork, @Warmup, @Measurement, @BenchmarkMode), why must you return or consume computed values (blackhole) to prevent the JIT from eliminating dead code, and why is System.nanoTime() in a loop NOT a valid Java benchmark (no warm-up, no fork isolation, GC noise, dead-code elimination) is fundamental knowledge for Java interviews and production banking work.
---