HotSpot decides which bytecode to compile using two per-method counters. Name them, explain why one is not enough, and describe what the runtime does when a counter crosses its threshold.
answer
- Two counters: entry count + backward-branch count
- Loop-only method has invocation count 1
- Overflow -> queue a compile task, keep running
- Back-edge overflow -> OSR keyed to bytecode index
- Thresholds behave like a rate, not a lifetime total
basics
~20 sAn invocation counter (incremented on method entry) and a back-edge counter (incremented on every backward branch, i.e. loop iteration). Invocations alone miss long-running loops in rarely called methods. Crossing a threshold files an asynchronous compilation request; back-edge overflow requests on-stack replacement.
solid answer
~50 sEach method carries an **invocation counter**, bumped on entry, and a **back-edge counter**, bumped whenever control flows backwards - once per loop iteration. Two counters are needed because hotness has two shapes: a small method called a million times, and a method called once that loops for minutes. An invocation-only policy would never notice the second. When a counter overflows its threshold the interpreter calls into the runtime, which decides whether to enqueue a compilation. Compilation is **asynchronous**: a compiler thread does the work while the interpreter keeps running the method, and the entry point is patched once the compiled code is installed. Overflow of the *back-edge* counter requests a special **on-stack replacement (OSR)** compilation keyed to the loop's bytecode index, so the loop that is already executing can be moved into compiled code instead of waiting for the next call. Thresholds are policy, not physics: HotSpot's counter thresholds are tunable and, in practice, the runtime treats them as a *rate* signal rather than a lifetime total.
code
text · 5 lines$ java -XX:+PrintCompilation Batch
112 1 3 java.lang.String::hashCode (49 bytes)
130 7 % 3 Batch::crunch @ 12 (58 bytes)
141 9 % 4 Batch::crunch @ 12 (58 bytes)
156 12 4 Batch::step (23 bytes)go deeper
Name the two counters and what each measures; knowing that crossing a threshold triggers compilation is enough.
Explain why both shapes of hotness need covering, that compilation is queued asynchronously, and that back-edge overflow specifically requests on-stack replacement.
Add the operational reality: thresholds behave like rate signals, warm-up depends on real traffic, and PrintCompilation is how you verify what actually got compiled.
Discuss the policy as a scheduling problem - bounded compiler threads and code cache versus a stream of promotion requests, and how load shape determines how quickly a fleet reaches steady state.
## The two counters HotSpot attaches profiling state to each method (in the method's metadata / MethodData structures). Two of those values drive compilation decisions: - **Invocation counter** - incremented in the interpreter's method entry sequence. It measures how often the method is *called*. - **Back-edge counter** - incremented on every *backward branch* in the bytecode. A `for`/`while`/`do` loop compiles to a conditional branch that jumps backwards to the loop head, so this counter effectively counts loop iterations, including iterations of loops nested inside the method. At lower optimization levels the same counters are also maintained by compiled code, so a method compiled cheaply can still be observed and promoted later. ## Why both are necessary Hotness is 'where the process spends its time', and there are two ways to accumulate time. Call-heavy code (a getter, a hash function, a parser step) accumulates it across many short invocations. Loop-heavy code accumulates it inside one invocation - the classic case being a `main` method with a single long-running loop, or a batch job that never returns until it is done. If the runtime counted only invocations, the loop-heavy shape would run interpreted forever, which was exactly the motivation for adding back-edge counting and OSR. ## What overflow actually does The counters are not compared against the threshold on every increment by expensive means; the interpreter uses cheap checks (in modern tiered mode, notification points chosen so the check happens periodically, controlled by flags such as `-XX:Tier0InvokeNotifyFreqLog` and `-XX:Tier0BackedgeNotifyFreqLog`). When a check indicates the threshold is crossed, control enters the runtime, which consults the compilation policy and typically **enqueues a compile task**: - Invocation-counter overflow requests a **standard compilation**: an nmethod entered at the method's normal entry point, used by future calls. - Back-edge-counter overflow requests an **OSR compilation** keyed to `(method, bytecode index of the loop back-edge)`: an nmethod entered in the middle of the method, so the already-running invocation can be transferred into it. Compilation happens on background compiler threads. The requesting thread does not wait; it continues in the interpreter (or in the currently running code) until the compiled version is installed. This keeps a compilation storm from turning into an application stall, at the cost of the application running unoptimized a little longer. ## Thresholds are a rate signal, not a lifetime total A naive reading of a flag like `-XX:CompileThreshold=10000` is 'the 10,000th call triggers compilation'. Two things complicate that. First, HotSpot historically **decayed** counters at safepoints, so counts that accumulate very slowly can be aged away and never reach the threshold - the threshold then effectively measures *frequency*, not lifetime total. Second, the modern tiered policy does not use a single raw threshold at all: it combines invocation and back-edge counts with the observed invocation *rate* and the current length of the compile queue, so a method under heavy load is promoted sooner than one dribbling along. The practical consequence is real: a lightly exercised service can stay interpreted far longer than a naive count would predict, and warm-up depends on load, not just on elapsed time. ## Observing it Compilation events are visible with `-XX:+PrintCompilation`, which prints one line per installed nmethod; OSR compilations are distinguished in that output (an `%` marker and the bytecode index at which the loop was entered). This is the usual way to confirm that the hot method you care about actually got compiled, and whether it arrived by the normal path or by OSR.
- Why can a method called a few thousand times over ten minutes in a quiet service still be interpreted, even though -XX:CompileThreshold is lower than that?Because the policy responds to rate, not to a lifetime total. Counters have historically been decayed periodically, and the tiered policy weighs invocation rate and compile-queue pressure, so slowly accumulated counts may never cross the bar. This is why warm-up is driven by traffic: a service must actually be exercised to reach steady-state performance.
- What happens to the counters when compiled code is thrown away and execution returns to the interpreter?The method goes back to being counted and profiled from the interpreter, so it can be re-compiled once it proves hot again, usually with the new information that invalidated the previous version. Repeated invalidation of the same method is a warning sign - the profile is unstable and the runtime is paying to compile it over and over.
saying these in an interview costs you the question
- Saying only invocation counts matter, so a loop in main is never optimized
- Reading -XX:CompileThreshold as an exact lifetime call count that always triggers compilation
- Claiming the calling thread blocks while the compiler runs
- Assuming the back-edge counter counts branches in general rather than backward branches
- Thinking counters are per-call-site rather than per-method