skip to content

HotSpot does not inline every call. Which properties of the call site and of the target method decide the outcome, and what practical size and depth limits apply?

level: middleimportance: should knowfreq 45%

answer

  1. Trivial 6 / MaxInlineSize 35 / FreqInlineSize 325 bytecodes
  2. HugeMethodLimit 8000 = never compiled
  3. MaxInlineLevel bounds nesting depth
  4. 'no static binding' = dispatch, no inline
  5. Structure first, flags last

basics

~20 s

Three families of limits: callee size (tiny methods almost always, small ones if the site is hot, big ones never), call-site hotness from the collected invocation profile, and inlining depth plus total expansion budget. On top of that, the target must be statically bindable and compilable.

solid answer

~50 s

HotSpot's inliner asks, per call site: can I bind this to exactly one method body, and is it worth the code? **Binding.** Static, private, constructor and provably-single-target virtual calls bind directly; otherwise the compiler needs a receiver-type profile and a guard, and a megamorphic site is not inlined at all (`no static binding`). **Size.** Roughly three bands: trivial bodies (a few bytecodes, e.g. accessors) go in almost unconditionally; small methods below `MaxInlineSize` (35 bytecodes by default) are inlined even when the site is not especially hot; larger ones up to `FreqInlineSize` (325) only if the site is hot. Nothing above that is inlined, and enormous methods are not even compiled (`DontCompileHugeMethods`, 8000 bytecodes). **Depth and budget.** `MaxInlineLevel` bounds nesting, recursion is bounded separately, and `InlineSmallCode` refuses a callee whose already-compiled code is large. The practical consequence: giant methods are optimization dead zones, both as callers and as callees.

code

text · 10 lines
text
-XX:MaxTrivialSize=6        // inlined unconditionally (accessors)
-XX:MaxInlineSize=35        // inlined even if not hot (bytecodes)
-XX:FreqInlineSize=325      // inlined only if the site is hot
-XX:MaxInlineLevel=15       // nesting depth cap
-XX:InlineSmallCode=2500    // refuse callees whose compiled code is bigger
-XX:-DontCompileHugeMethods // otherwise >8000-bytecode methods stay interpreted

reasons seen in -XX:+PrintInlining:
  inline (hot) | accessor | too big | hot method too big
  no static binding | not compilable | recursive inlining is too deep

go deeper

for a junior

Know that there are limits at all: small and frequently called methods get inlined, big ones do not.

for a middle

Name the three families — size, hotness, depth — and the fact that size is counted in bytecodes with a not-hot threshold around 35 and a hot threshold around 325.

for a senior

Add the huge-method compilation cliff, InlineSmallCode, profile pollution, and how to read PrintInlining reasons on a real workload.

for a principal

Treat inlining budget as a design constraint on hot paths: keep hot cores small and monomorphic, and resist global flag tuning that trades a local win for compile time and code-cache pressure.

## The inliner's two questions For each call site the compiler decides (1) *can* it inline — is there exactly one method body this site will execute, and is that body available and compilable — and (2) *should* it — is the expected benefit worth the code growth and compile time. ## Can it: binding Calls compiled from `invokestatic`, `invokespecial` (constructors, private methods, super calls) have exactly one target by construction. Virtual and interface calls need help: class-hierarchy analysis may show only one loaded implementation, or the recorded receiver-type profile may show one dominant type that the compiler then speculates on behind a class-check guard. If the site has seen many types, the inliner reports `no static binding` and emits a dispatching call. Native methods and methods marked not-compilable are also refused. ## Should it: size bands HotSpot measures callee size in **bytecodes**, not source lines: - **Trivial** — up to `MaxTrivialSize` (6 bytes): accessors and the like, inlined essentially always. - **Small** — up to `MaxInlineSize` (35 bytes by default): inlined even at call sites that are not hot. - **Hot** — up to `FreqInlineSize` (325 bytes): inlined only when the profile shows the site is frequently executed. - **Too big** — beyond that, refused with `too big` / `hot method too big`. A separate ceiling, `DontCompileHugeMethods` with `HugeMethodLimit` (8000 bytecodes), stops the JIT from compiling a monster method at all — it stays interpreted, which is dramatically worse than merely not being inlined. Generated code, giant switch statements, and hand-unrolled loops are the usual victims. ## Should it: depth and budget `MaxInlineLevel` (15 in current HotSpot) bounds how deep a chain of nested inlines may go, so a pipeline of many thin wrappers can run out of depth before reaching the code that matters. Recursive inlining has its own small bound. `InlineSmallCode` refuses to inline a callee whose previously compiled machine code exceeds a threshold, on the theory that it is big in practice regardless of bytecode size. C2 also tracks overall node growth for the compilation and stops expanding when the compilation is getting too large. ## Profile quality matters as much as size An inlining decision is only as good as the profile behind it. If a shared helper method is invoked from many places with many receiver types, its own inner call sites accumulate a polluted profile; a caller that would have been monomorphic in isolation inherits a megamorphic-looking profile and loses inlining. This is why duplicating a small helper occasionally produces a large, surprising speed-up. ## Reading the decisions `-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining` prints one line per call site with the reason: `inline (hot)`, `accessor`, `too big`, `callee is too large`, `no static binding`, `recursive inlining is too deep`, `not compilable`, `already compiled into a big method`. `-XX:+LogCompilation` gives the same data as XML for tooling. ## What to do with this Tune flags last, structure first. Split huge methods so the hot core is small enough to be inlined and compiled; keep the hot inner loop's dispatch monomorphic; avoid deep towers of trivial wrappers on the hottest path; and confirm with `PrintInlining` rather than assuming. Raising `MaxInlineSize` or `FreqInlineSize` globally usually trades a local win for longer compiles, more code cache and worse instruction locality.

  • Why is a method above the huge-method limit so much worse than a method that is merely too big to inline?
    A method that is too big to inline is still JIT-compiled itself, so its own body gets optimized. A method past HugeMethodLimit is not compiled at all by default and keeps running in the interpreter, which is roughly an order of magnitude slower. Splitting the hot part into a separate small method restores compilation.
  • Would you raise MaxInlineSize in production to get more inlining?
    Rarely, and never as a first move. It applies globally, lengthens compilations, grows the code cache and hurts instruction-cache locality, and the benefit is usually confined to a few call sites. Restructure the hot path — smaller hot methods, monomorphic dispatch — and if you do change a flag, measure with the flag as the only variable.

saying these in an interview costs you the question

  • Measuring callee size in source lines rather than bytecodes
  • Assuming any method under 35 lines is always inlined regardless of hotness or dispatch
  • Not knowing that very large methods are not compiled at all
  • Reaching for -XX:MaxInlineSize as the first fix instead of restructuring

context