Files

jmh-basics

Companion code for the ankurm.com post "JMH Tutorial: Writing Java Microbenchmarks That Don't Lie." Module in java-core-examples, the Java-core series. There is deliberately no docs/ folder: the deeper material lives in collapsible sections inside the post itself.

Versions this was built and tested against

Component Version Notes
JDK 25.0.5+8 (Temurin, LTS) Compiler blackholes were auto-detected on this JVM; see output/02.
JMH 1.37 Latest GA on Maven Central; released 3 Aug 2023.
JUnit Jupiter 5.11.0
Maven 3.9.11
Host 2 vCPU shared sandbox Read the shape of the numbers, not the third significant digit.

Quickstart

export JAVA_HOME=/path/to/jdk-25
mvn -q package -DskipTests                       # builds target/benchmarks.jar (shaded, runnable)
java -cp target/classes com.ankurm.jmhbasics.NaiveTimingDemo
java -jar target/benchmarks.jar DeadCodeBenchmark
java -jar target/benchmarks.jar AllocationBenchmark -prof gc

scripts/run-all.sh regenerates every file in output/ (about 8 minutes on 2 vCPUs). scripts/run.sh <ClassName> runs one demo class ad hoc.

What's in here

File What it shows
NaiveTimingDemo.java The System.nanoTime() loop: a discarded result reports 0.000 ns/op, and the kept version changes from round to round while the JIT warms up.
DeadCodeBenchmark.java Empty baseline, discarded result, returned result, Blackhole, and constant folding: Math.pow of a constant folds, Math.log of a constant does not, and a final instance field IS folded like a constant (an early draft of the Javadoc claimed the opposite; the measurement and JMHSample_10 both disagreed).
LifecycleDemo.java Counts how often every @Setup/@TearDown level fires, by running JMH with forks(0) in-process.
ForkingBenchmark.java One shared call site, two implementations, run with -f 0 and -f 1 to show type-profile pollution.
StateScopeBenchmark.java Scope.Thread vs Scope.Benchmark with two threads.
AllocationBenchmark.java Escape analysis: the same new Point(..) allocates 0 or 24 bytes per call depending on whether it escapes. Use -prof gc.
SumBenchmark.java @Param over three input sizes, @Setup outside the measured region.
SortBenchmark.java Warmup trace (-wi 10 -w 200ms) and three benchmark modes in one run.
JmhBasicsTest.java Pins the deterministic claims: lifecycle counts, generated benchmark list, benchmark bodies. No timing assertions.
output/01-12 Captured runs of all of the above.

Notes worth knowing before reading the post

  • Each @BenchmarkMode value is a separate benchmark entry. SortBenchmark.sortCopy carries three modes, so the generated META-INF/BenchmarkList has three lines for it (21 entries for 19 methods; the test pins this).
  • Run LifecycleDemo from the shaded jar, not from target/classes: JMH is not on the plain classpath.
  • Timing figures drift. Several error margins in output/ are wide on this shared host. The post draws conclusions only from differences far larger than the error bars.

License

MIT - see the repo-wide LICENSE.