JMH Microbenchmarking Pitfalls — Interview Questions
Reviewed by Gurusankar M.
Short Answer
A hand-written loop timed with System.currentTimeMillis() gets skewed by at least three JIT-related effects: the code runs interpreted or under low-tier C1 compilation for the first many iterations before C2 kicks in, so early iterations are much slower than steady-state; the JIT can eliminate 'dead' code whose result is never used, silently benchmarking nothing; and constant folding can pre-compute a result at compile time if inputs look constant to the optimizer, again measuring nothing real. JMH (Java Microbenchmark Harness) exists specifically to defeat these: it runs dedicated warm-up iterations before measuring, forks a fresh JVM process per benchmark to avoid cross-benchmark JIT pollution, and uses Blackhole to consume results so the JIT can't optimize the 'unused' computation away.
On this page
Concept Made Simple
Coffee Chat Question
“Why does timing a loop with System.currentTimeMillis() lie to you, and how does JMH avoid those traps?”
Remember It Faster
Mind Map Answer
The core problem is that modern JVMs are adaptive optimizers, and a naive timing loop measures the optimizer warming up, not the code's steady-state cost — plus the optimizer is smart enough to delete work it can prove is pointless, which a benchmark's whole purpose is to avoid.
- JIT warm-up
- Early iterations run interpreted/C1 — much slower than C2 steady-state
- Dead-code elimination
- Unused results get optimized away — you benchmark nothing
- Constant folding
- Compile-time-constant-looking inputs get pre-computed away
- JMH's fix
- Warm-up iterations, forked JVMs, Blackhole to consume results
Key takeaway: 'I benchmarked it and it's faster' is only credible with a methodology that accounts for JIT warm-up and prevents dead-code elimination — naming JMH by name, and why it's needed, is what separates a real performance claim from a guess.
Learn by Doing
Hands-on Keyboard
@Benchmark
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@Fork(1)
public void stringConcat(Blackhole bh) {
String result = "a" + "b" + counter++;
bh.consume(result); // prevents dead-code elimination of "result"
}
// Naive (misleading) alternative:
long start = System.currentTimeMillis();
for (int i = 0; i < 1_000_000; i++) {
String s = "a" + "b" + i; // JIT may eliminate this — result is unused
}
long elapsed = System.currentTimeMillis() - start; // measures ~nothing reliableThink Beyond the Expected
What If?
If you just print the result inside the loop, does that fix dead-code elimination?
It helps but isn't sufficient or standardized — printing has its own I/O overhead that pollutes the measurement, and the JIT can still optimize surrounding code in ways a manual loop doesn't control for. Blackhole.consume() is specifically designed to force the JIT to treat the value as used without the side effects of real I/O, which is why JMH is the accepted standard rather than ad-hoc print statements.
Real World
This comes up whenever a PR claims a performance improvement backed only by 'I timed it before and after with System.currentTimeMillis()' — without JMH, that claim could easily be measuring JIT warm-up noise, dead-code elimination artifacts, or just JVM-to-JVM variance, and a careful reviewer will ask for a JMH benchmark before trusting a performance-motivated code change, especially one that trades off readability for speed.
Real Talk from Guru
If someone shows me a 'proof' with a manual timing loop, my first question is whether the JIT could have eliminated the work being measured — it's not pedantry, it's the single most common way well-intentioned benchmarks lie. I'd reach for JMH before making any performance claim I actually plan to defend.
Interviewer's Expectation
Keywords they're listening for:
- Names JIT warm-up as a source of skew in naive timing
- Explains dead-code elimination and constant folding as measurement risks
- Knows JMH's mitigations: warm-up iterations, forking, Blackhole
- Treats unverified performance claims with appropriate skepticism
Common Mistakes
- Trusting a System.currentTimeMillis() loop as a real performance measurement
- Not accounting for JIT warm-up before measuring
- Writing benchmark code whose result is never consumed, inviting dead-code elimination
Best Practices
- Use JMH for any performance claim meant to be trusted or defended
- Always include warm-up iterations before measuring
- Consume benchmark results (Blackhole) to prevent the JIT from eliminating them
Follow-up Questions
- Why does JMH fork a separate JVM process per benchmark instead of running them all in one?
- What does Blackhole.consume() actually do to prevent dead-code elimination?
- How would you benchmark code that has side effects, where JMH's isolation doesn't neatly apply?
Continue Learning with AI
Take this question deeper with your favourite AI assistant. Pick a depth, copy the prompt, or open it directly — AI is your learning companion, not a shortcut.
Plain-language foundations
I'm preparing for a software engineering interview and want to understand this from scratch, as a beginner. Topic: JIT (JVM) Interview question: "Why does timing a loop with System.currentTimeMillis() lie to you, and how does JMH avoid those traps?" Please: 1. Explain the core idea in simple, plain language, using an everyday analogy. 2. Define any technical terms you use. 3. Walk through one small, concrete example. 4. Finish with a single sentence I can easily remember. Keep the tone friendly and assume I'm new to this topic.
Was this answer helpful?
Affiliate links
Tools for Your Developer Workspace
Equipment for a comfortable, productive setup.
As an Amazon Associate I earn from qualifying purchases.
- MonitorView on Amazon : BenQ EW2790U 27-inch Monitor (opens in a new tab)
BenQ EW2790U 27-inch Monitor
- LaptopView on Amazon : Acer Aspire One Laptop (opens in a new tab)
Acer Aspire One Laptop
- Office ChairView on Amazon : Wakefit Gravita Ergonomic Office Chair (opens in a new tab)
Wakefit Gravita Ergonomic Office Chair