skip to content

Explain @State scopes (Benchmark, Thread, Group) and the @Setup/@TearDown lifecycle with their Levels in JMH.

level: seniorimportance: should knowfreq 42%

answer

  1. State defeats constant folding; injected into @Benchmark
  2. Scope: Benchmark=shared / Thread=per-thread / Group=per cooperating group
  3. Setup/TearDown Levels: Trial (once) / Iteration / Invocation
  4. Invocation-level = on timed path, dangerous for fast ops
  5. Mutable Benchmark-scope state => contention + false sharing

basics

~20 s

A @State class holds the data your benchmark uses. Its scope says who shares one instance: Benchmark = all threads share one, Thread = each thread gets its own, Group = one per group of cooperating threads. @Setup methods prepare state before the benchmark and @TearDown cleans up after; each can run at Trial, Iteration, or Invocation level depending on how often you need it.

solid answer

~50 s

JMH keeps benchmark inputs in @State objects so the JIT can't constant-fold them, and the scope controls sharing across threads. Scope.Benchmark means a single shared instance across all worker threads — use it for read-only shared data, but beware contention and false sharing on mutable fields. Scope.Thread gives each thread its own instance, the safest default for per-thread mutable state. Scope.Group ties one instance to a thread group defined with @Group, for asymmetric producer/consumer benchmarks. State is prepared with @Setup and cleaned up with @TearDown, each parameterized by Level: Level.Trial (once per fork/whole benchmark run, the default), Level.Iteration (before/after each measurement or warmup iteration), and Level.Invocation (around every single @Benchmark call — powerful but high-overhead and easy to misuse, since the timing wrapper itself can distort fast benchmarks). The golden rule: never reset expensive state at Invocation level if it dwarfs the measured work, and remember @Setup at Invocation runs on the timed path.

code

java · 26 lines
java
import org.openjdk.jmh.annotations.*;
import java.util.*;

public class StateDemo {

    @State(Scope.Thread)            // each thread gets its own list
    public static class Data {
        List<Integer> values;

        @Setup(Level.Trial)        // built once per fork, off the timed path
        public void build() {
            values = new ArrayList<>();
            for (int i = 0; i < 10_000; i++) values.add(i);
        }

        @TearDown(Level.Trial)
        public void check() { /* assert/cleanup */ }
    }

    @Benchmark
    public int sum(Data d) {       // state injected; defeats constant folding
        int s = 0;
        for (int v : d.values) s += v;
        return s;                  // returned -> consumed
    }
}

go deeper

for a junior

Knows a @State object holds the benchmark's inputs and that @Setup/@TearDown initialize and clean up.

for a middle

Can name the three scopes and the three Levels and pick Trial vs Iteration sensibly.

for a senior

Understands why mutable Scope.Benchmark state creates contention/false sharing, when Group scope is needed, and why Level.Invocation is risky for fast benchmarks.

for a principal

Designs benchmark state to isolate exactly the variable under study, reasons about cache-line effects and amortization of resets, and reviews others' benchmarks for accidental contention or constant folding.

## Why state objects exist A benchmark needs **inputs** — an array to sort, a map to query, a string to parse. If you write those as local constants, the JIT optimizer can **constant-fold** them (precompute the answer) and your benchmark measures nothing. JMH solves this by putting inputs in a **`@State`** class: a class annotated `@State(scope)` whose fields JMH treats as opaque, defeating constant folding. The state object is **injected** as a parameter into your `@Benchmark` method (or you make the benchmark class itself the state). ## The three scopes — who shares an instance Benchmarks can run with multiple worker threads (`@Threads(n)`). The **`Scope`** decides how many state instances exist and who shares them: - **`Scope.Benchmark`** — **one** instance shared by **all** threads. Correct for *read-only* shared input. For *mutable* fields it introduces real concurrency: lock contention, cache-line **false sharing** (independent fields on the same 64-byte cache line ping-pong between cores), and you may end up benchmarking the synchronization rather than the work. - **`Scope.Thread`** — **each thread gets its own** instance. The safe default when threads mutate their state independently; no cross-thread interference. - **`Scope.Group`** — **one instance per group** of cooperating threads. Threads are partitioned into groups with `@Group("name")` and assigned roles with `@GroupThreads`. This enables **asymmetric** benchmarks — e.g. some threads run a producer `@Benchmark` and others a consumer `@Benchmark`, all sharing one group state (like a queue). ## The lifecycle: @Setup and @TearDown A `@State` class may declare **`@Setup`** methods (run before benchmarking to initialize) and **`@TearDown`** methods (run after, to verify or release). Each takes a **`Level`** controlling *how often* it fires: - **`Level.Trial`** (default) — once per **fork/trial**, i.e. around the entire run of that benchmark in that JVM. Use for expensive one-time setup (build a big dataset once). - **`Level.Iteration`** — around **each iteration** (every warmup and measurement iteration). Use to reset state that accumulates within an iteration. - **`Level.Invocation`** — around **every single `@Benchmark` invocation**. Powerful but dangerous: the setup/teardown code runs **on the timed path's edges**, the call adds overhead, and JMH explicitly warns it's only valid when the per-invocation work is *large* relative to the setup. For nanosecond-scale benchmarks, Invocation-level hooks can dominate and corrupt the measurement. There are also no ordering/visibility guarantees suitable for ultra-fine timing. ## Putting it together — common patterns - Read-only shared lookup table → `Scope.Benchmark`, `@Setup(Level.Trial)` builds it once. - Per-thread mutable accumulator → `Scope.Thread`. - Producer/consumer over a shared queue → `Scope.Group` + `@Group`/`@GroupThreads`. - "I must restore a mutable structure between calls" → tempting `Level.Invocation`, but first ask whether the reset cost swamps the measured op; if so, redesign (e.g. measure a batch, or recreate at Iteration level and accept amortization). ## Pitfalls - Mutable `Scope.Benchmark` state silently turns a single-thread benchmark into a contention benchmark. - `Level.Invocation` setup is counted by the harness as part of the surrounding machinery; for tiny operations the wrapper overhead is larger than the thing measured. - Forgetting that `@State` must be a public class with a public no-arg constructor (JMH instantiates it).

  • Your benchmark mutates a shared HashMap with @Threads(8) and Scope.Benchmark. What are you really measuring?
    Largely the synchronization/contention and false-sharing cost on the shared instance, not the pure operation. Use Scope.Thread for independent per-thread state, or accept that you're benchmarking the concurrent structure on purpose.
  • When is Level.Invocation justified despite its overhead?
    Only when each invocation's work is large relative to the setup/teardown — e.g. a multi-millisecond operation that genuinely needs fresh state each call. For sub-microsecond ops the wrapper overhead corrupts the result.

saying these in an interview costs you the question

  • Using Scope.Benchmark for mutable state in a multi-threaded benchmark without realizing you measure contention
  • Defaulting to Level.Invocation for resets on nanosecond-scale benchmarks (overhead dominates)
  • Believing @Setup never affects timing (Invocation-level runs around the timed call)
  • Thinking state is optional decoration rather than the defense against constant folding

context