Working with JMH (Microbenchmarking)
1. Setting Up JMH Project
| Step | Detail |
|---|---|
| Generate | mvn archetype:generate -Dgroup=org.openjdk.jmh -DartifactId=jmh-archetype |
| Dependencies | jmh-core, jmh-generator-annprocess |
| Build | Shade JMH runner JAR |
| Run | java -jar target/benchmarks.jar |
2. Creating Benchmark Methods (@Benchmark)
| Rule | Detail |
|---|---|
| Return result | Or use Blackhole |
| No I/O | Unless intentionally measuring it |
3. Using Benchmark Modes
| Mode | Reports |
|---|---|
| Throughput | ops/time |
| AverageTime | time/op |
| SampleTime | Sampled time distribution |
| SingleShotTime | One iteration (cold) |
| All | All of the above |
4. Configuring Warmup (@Warmup)
| Param | Default |
|---|---|
| iterations | 5 |
| time | 10 s |
| batchSize | 1 |
| Purpose | Reach steady state (JIT, caches) |
5. Configuring Measurement (@Measurement)
| Param | Use |
|---|---|
| iterations | 5–10 typical |
| time | 10 s typical |
| forks | JVM forks (default 5) |
| threads | Concurrent worker count |
6. Using State Objects (@State)
Example: State
@State(Scope.Benchmark)
public static class MyState {
String value;
@Setup public void init() { value = "hello"; }
}
| Aspect | Detail |
|---|---|
| Lifecycle | Linked to Scope |
| Injection | Pass as @Benchmark method param |
7. Understanding Scope
| Scope | Sharing |
|---|---|
| Benchmark | One instance for all threads |
| Group | Shared in @Group methods |
| Thread | Per-thread instance |
8. Using Setup and TearDown (@Setup, @TearDown)
| Level | Runs |
|---|---|
| Trial | Once per benchmark run |
| Iteration | Each iteration |
| Invocation | Every @Benchmark call (use sparingly — bias risk) |
9. Avoiding Dead Code Elimination (Blackhole)
Example: Blackhole
@Benchmark
public void multi(Blackhole bh) {
bh.consume(work1());
bh.consume(work2());
}
| Issue | Fix |
|---|---|
| Unused result | Return it OR Blackhole.consume() |
| Constant folding | Read input from @State |
10. Running Benchmarks
| Command | Use |
|---|---|
java -jar benchmarks.jar | Run all |
java -jar benchmarks.jar Pattern | Filter by name regex |
-prof gc | Allocation profiler |
-prof async:libPath=...;output=flamegraph | async-profiler |
-rf json -rff out.json | JSON output |
11. Analyzing Results
| Field | Meaning |
|---|---|
| Score | Mean throughput / time |
| Error | 99.9% confidence half-width |
| Ratio | Compare to baseline |
| Visualize | jmh-visualizer (web UI) |
12. Understanding JMH Pitfalls
Warning: Microbenchmarks are easy to write but hard to interpret. Always: (1) cross-check with macro-benchmark, (2) verify dead-code isn't elided, (3) use multiple forks, (4) account for warmup, (5) compare on the same JVM/hardware.
| Pitfall | Effect |
|---|---|
| Constant folding | JIT precomputes result |
| Inlining hides cost | Use Blackhole |
| No warmup | Measures interpreter |
| Single fork | Class init bias |