Eight benchmark/demo classes, a deterministic test suite, and 12 captured output files generated by scripts/run-all.sh on JDK 25.0.5 (Temurin) with JMH 1.37. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6Fj2JNt7dRev4pZPe
3.2 KiB
3.2 KiB
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
@BenchmarkModevalue is a separate benchmark entry.SortBenchmark.sortCopycarries three modes, so the generatedMETA-INF/BenchmarkListhas three lines for it (21 entries for 19 methods; the test pins this). - Run
LifecycleDemofrom the shaded jar, not fromtarget/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.