C2 and the Sea-of-Nodes IR
Purpose
Decide why a compilation came out the way it did, from the mechanism rather than from
folklore. The failure this prevents is the confident non-fix: a runbook that sets
-XX:CompileThreshold under tiered compilation and changes nothing at all, or a
refactoring done because "the JIT will optimise it" when the JIT never performs
algorithmic changes.
Start with tier/version, call-site inlining and escape state, then follow the failing
transformation: type/profile stability, alias/memory dependencies, loop/range checks,
vectorization, macro expansion, matching, scheduling and register pressure can each own the
result. The phase model routes evidence; it is not a three-question completeness claim.
Workflow
The authoring baseline is HotSpot C2 on JDK 25 (reference measurements use Temurin
25.0.3); these are implementation details, not Java language guarantees. Before running
recipes, inspect the project's toolchain, CI/runtime image and actual java -version,
including vendor/update, product versus debug build, compiler and effective flags.
Level 4 can be JVMCI rather than C2; route that compiler's internals to graalvm-jit.
Do not upgrade the target runtime or enable preview features to match this baseline.
For another release, check java -Xlog:help and flag availability first; unavailable
diagnostics require an alternative evidence source, not an assumed result.
- Establish the compilation history before anything else. Run
-Xlog:jit+compilation=debug/PrintCompilation and correlate compilation ID, level, OSR,
invalidation and timestamp. One tier-1/3 line does not prove the method's final/current
state; later versions can coexist or be made non-entrant.
- If it is stuck in tier 3, compare real counters against the tier-4 thresholds
(
Tier4InvocationThreshold, Tier4CompileThreshold) rather than assuming a compiler bug.
- If it reached tier 4, check inlining on the hot call site with
-XX:+PrintInlining,
reading the tier-4 tree, not the tier-3 one above it. C2 names the limit it applied:
too big is MaxInlineSize at a cold site, hot method too big is FreqInlineSize,
already compiled into a big method is InlineSmallCode, inlining too deep is
MaxInlineLevel. callee is too large is C1's verdict and says nothing about C2.
- If an allocation appears to survive, get the escape evidence before theorising. On a debug
build,
-XX:+PrintEscapeAnalysis with -XX:+PrintEliminateAllocations answers two
different questions; both are develop flags, so a product JVM refuses to start on
them. On a shipping runtime use differential checks instead — allocation profiles are
sampled and absence is not proof. Compare normalized allocated bytes/events under
controlled compilation and use generated code/IR where justified. ArgEscape — passed to a call that was
not inlined — normally remains heap-allocated even when the callee never stores the
reference.
- If
made not entrant: uncommon trap recurs on the same method, treat it as
deoptimisation, not as a threshold to tune. made not entrant: not used is the tier-3
code being retired by the tier-4 version and is normal. Investigate with
-Xlog:deoptimization=debug where supported or the JFR jdk.Deoptimization event first;
verify event availability and recording settings on the target runtime.
- Isolate one factor at a time in a disposable experiment before attributing a cost:
process-wide
-XX:-DoEscapeAnalysis, -XX:-EliminateAllocations, -XX:-Inline and
-XX:TieredStopAtLevel=1 radically change compilation and are not production fixes. See
references/jit-diagnosis-recipes.md.
- Confirm every number against the runtime you are actually on with
-XX:+PrintFlagsFinal -version, then measure any change with JMH — never with an
isolated System.nanoTime().
Rules
- Tiered compilation is the default on every supported release including JDK 25. Under it
-XX:CompileThreshold is accepted without error and without effect. Never prescribe
it. Treat tier-specific thresholds and CompileThresholdScaling as broad diagnostic
experiments whose profile quality, compile CPU/queue and code-cache costs must be measured.
- There are five numbered levels (0-4). A common hot path is 0 → 3 → 4, while policy can use
levels 1/2 and OSR separately. Thresholds scale with queue pressure (
Tier3LoadFeedback, Tier4LoadFeedback),
so under a start-up burst a method can sit below a threshold that its counters would have
cleared on an idle JVM.
- The JIT does inlining, constant folding, escape analysis and vectorisation. It does not
change algorithms (O(n²) stays O(n²)), does not swap a
List for a Map, and does not
remove I/O or a query. Do not assume repeated string concatenation, collection choice or
asymptotic complexity will be redesigned across loop iterations.
- Escape analysis has three states —
NoEscape, ArgEscape, GlobalEscape — not a binary.
NoEscape is necessary for C2's scalar replacement of an allocation, not sufficient:
scalar replaceability and elimination must also succeed. A surviving allocation does
not prove escape, and storing into a field of another non-escaping object does not
automatically imply GlobalEscape.
- Inlining is a precondition for escape analysis reaching its best result: after inlining the
call boundary is gone, so an argument-passed object can be reclassified
NoEscape.
- Receiver-type width, probability, compiler profile limits and speculative guards determine
polymorphic inlining. Three observed types is a useful megamorphic warning on common builds,
not a language-level cutoff. Do not change extensibility to
final without showing the
target call-site decision and architectural/API consequence.
- Escape analysis/scalar replacement occur before matching in the inspected C2 pipeline; phase
numbering is a teaching model and internal passes can be repeated/reordered across releases.
A successfully eliminated allocation has no allocation instruction in final machine code.
- C2's register allocator is graph colouring, Chaitin-Briggs (
opto/chaitin.cpp). Linear
scan is C1's technique. Do not describe C2 as linear scan.
- Strip mining supports safepoint polling in counted loops and interacts with loop optimization.
On verified JDK 25.0.3,
UseCountedLoopSafepoints is true for G1/ZGC/Shenandoah and false
for Parallel/Serial — not one JDK-wide default.
DoEscapeAnalysis has defaulted to true since JDK 6 Update 23 (~2010). Any material
presenting it as a recent feature is out of date.
- Do not write a parser against
PrintEscapeAnalysis / PrintEliminateAllocations output —
it is internal compiler diagnostics and the exact strings vary between builds. Read it, then
cross-check against the source of the method.
- In production, use allocation-rate/profile deltas as evidence, accounting for sampling,
TLAB/outside-TLAB coverage, compilation state and workload. Absence of a sampled allocation
cannot by itself confirm scalar replacement.
- A benchmark whose result is neither returned nor consumed by a
Blackhole can have its whole
body removed by dead code elimination. That is the default failure mode of any measurement in
this area, not an edge case.
- Aggregate CPU overhead and p99 latency are different quantities. Never derive one from the
other without an explicit queueing model.
Decision/validation checklist
- If source, compilation identity or runtime evidence is missing, state the gap and the
smallest capture that can resolve it. Keep the proposed cause conditional; do not
recommend an inlining/threshold flag as a confirmed fix from source shape alone.
- Pin JDK vendor/update, compiler (C2 versus JVMCI), flags, compilation ID/level and profile
maturity; reproduce after warm-up and after deoptimization/recompilation.
- State whether evidence is bytecode, ideal graph, compiler log, assembly, allocation sample or
benchmark. Each can falsify different hypotheses and none substitutes for all others.
- Test semantic edge cases before refactoring for the compiler: exceptions, overflow, NaN,
aliasing, concurrency/publication and uncommon paths can be the guards preventing an opt.
- Validate end-to-end throughput/tail/CPU/code-cache effects. A microbenchmark win under forced
directives is not authorization for a process-wide production flag.
Deliver the relevant compile ID and artifact location, the observed decision, the inferred
blocking mechanism, and one confirming or falsifying check. For a proposed change, include
the semantic constraints and before/after metric; say explicitly when it remains untested.
References
- C2 phases and the IR — the five tiers, the seven-phase
pipeline, the three edge types of the sea-of-nodes graph, the inlining size limits and the
three escape states, as tables. Read when you need to say where in the pipeline a decision
was made, or which limit a specific inlining verdict came from.
- JIT diagnosis recipes — the exact flag combinations
for tier, inlining and escape diagnosis, the factor-isolation runs, and the correct threshold
tuning flags. Read when you are about to run the JVM to answer one of these questions.
Authoritative sources: OpenJDK C2 sources,
HotSpot compiler control,
and JEP 165: Compiler Control.
1---2name: c2-sea-of-nodes3description: How HotSpot actually executes and compiles: the runtime-generated template interpreter, C2's sea-of-nodes IR, a release-scoped diagnostic map of compilation phases, and why a given transformation fired or did not. Use when a method is believed to be "not optimised", when an allocation that looks eliminable still shows up in allocation profiling, when a hot call site reports `too large` or stays non-inlined, when `made not entrant` repeats on the same method, when someone prescribes `-XX:CompileThreshold` under tiered compilation, or when explaining why the JIT did not fix an O(n^2) loop. Does not cover the tiered pipeline, warm-up and code cache sizing (jit-compilation), reading the compiler's own decision logs end to end (compilation-and-inlining-logs), the emitted machine code (reading-jit-assembly), or the bytecode the compiler consumes (jvm-bytecode).4---56# C2 and the Sea-of-Nodes IR78## Purpose910Decide _why_ a compilation came out the way it did, from the mechanism rather than from11folklore. The failure this prevents is the confident non-fix: a runbook that sets12`-XX:CompileThreshold` under tiered compilation and changes nothing at all, or a13refactoring done because "the JIT will optimise it" when the JIT never performs14algorithmic changes.1516Start with tier/version, call-site inlining and escape state, then follow the failing17transformation: type/profile stability, alias/memory dependencies, loop/range checks,18vectorization, macro expansion, matching, scheduling and register pressure can each own the19result. The phase model routes evidence; it is not a three-question completeness claim.2021## Workflow2223The authoring baseline is HotSpot C2 on JDK 25 (reference measurements use Temurin2425.0.3); these are implementation details, not Java language guarantees. Before running25recipes, inspect the project's toolchain, CI/runtime image and actual `java -version`,26including vendor/update, product versus debug build, compiler and effective flags.27Level 4 can be JVMCI rather than C2; route that compiler's internals to `graalvm-jit`.28Do not upgrade the target runtime or enable preview features to match this baseline.29For another release, check `java -Xlog:help` and flag availability first; unavailable30diagnostics require an alternative evidence source, not an assumed result.31321. **Establish the compilation history before anything else.** Run33 `-Xlog:jit+compilation=debug`/`PrintCompilation` and correlate compilation ID, level, OSR,34 invalidation and timestamp. One tier-1/3 line does not prove the method's final/current35 state; later versions can coexist or be made non-entrant.362. **If it is stuck in tier 3, compare real counters against the tier-4 thresholds**37 (`Tier4InvocationThreshold`, `Tier4CompileThreshold`) rather than assuming a compiler bug.383. **If it reached tier 4, check inlining on the hot call site** with `-XX:+PrintInlining`,39 reading the **tier-4** tree, not the tier-3 one above it. C2 names the limit it applied:40 `too big` is `MaxInlineSize` at a cold site, `hot method too big` is `FreqInlineSize`,41 `already compiled into a big method` is `InlineSmallCode`, `inlining too deep` is42 `MaxInlineLevel`. `callee is too large` is C1's verdict and says nothing about C2.434. **If an allocation appears to survive, get the escape evidence before theorising.** On a **debug44 build**, `-XX:+PrintEscapeAnalysis` with `-XX:+PrintEliminateAllocations` answers two45 different questions; both are `develop` flags, so a product JVM refuses to start on46 them. On a shipping runtime use differential checks instead — allocation profiles are47 sampled and absence is not proof. Compare normalized allocated bytes/events under48 controlled compilation and use generated code/IR where justified. `ArgEscape` — passed to a call that was49 not inlined — normally remains heap-allocated even when the callee never stores the50 reference.515. **If `made not entrant: uncommon trap` recurs on the same method, treat it as52 deoptimisation**, not as a threshold to tune. `made not entrant: not used` is the tier-353 code being retired by the tier-4 version and is normal. Investigate with54 `-Xlog:deoptimization=debug` where supported or the JFR `jdk.Deoptimization` event first;55 verify event availability and recording settings on the target runtime.566. **Isolate one factor at a time in a disposable experiment** before attributing a cost:57 process-wide `-XX:-DoEscapeAnalysis`, `-XX:-EliminateAllocations`, `-XX:-Inline` and58 `-XX:TieredStopAtLevel=1` radically change compilation and are not production fixes. See59 `references/jit-diagnosis-recipes.md`.607. **Confirm every number against the runtime you are actually on** with61 `-XX:+PrintFlagsFinal -version`, then measure any change with JMH — never with an62 isolated `System.nanoTime()`.6364## Rules6566- Tiered compilation is the default on every supported release including JDK 25. Under it67 `-XX:CompileThreshold` is accepted **without error and without effect**. Never prescribe68 it. Treat tier-specific thresholds and `CompileThresholdScaling` as broad diagnostic69 experiments whose profile quality, compile CPU/queue and code-cache costs must be measured.70- There are five numbered levels (0-4). A common hot path is 0 → 3 → 4, while policy can use71 levels 1/2 and OSR separately. Thresholds scale with queue pressure (`Tier3LoadFeedback`, `Tier4LoadFeedback`),72 so under a start-up burst a method can sit below a threshold that its counters would have73 cleared on an idle JVM.74- The JIT does inlining, constant folding, escape analysis and vectorisation. It does **not**75 change algorithms (O(n²) stays O(n²)), does not swap a `List` for a `Map`, and does not76 remove I/O or a query. Do not assume repeated string concatenation, collection choice or77 asymptotic complexity will be redesigned across loop iterations.78- Escape analysis has three states — `NoEscape`, `ArgEscape`, `GlobalEscape` — not a binary.79 `NoEscape` is necessary for C2's scalar replacement of an allocation, not sufficient:80 scalar replaceability and elimination must also succeed. A surviving allocation does81 not prove escape, and storing into a field of another non-escaping object does not82 automatically imply `GlobalEscape`.83- Inlining is a precondition for escape analysis reaching its best result: after inlining the84 call boundary is gone, so an argument-passed object can be reclassified `NoEscape`.85- Receiver-type width, probability, compiler profile limits and speculative guards determine86 polymorphic inlining. Three observed types is a useful megamorphic warning on common builds,87 not a language-level cutoff. Do not change extensibility to `final` without showing the88 target call-site decision and architectural/API consequence.89- Escape analysis/scalar replacement occur before matching in the inspected C2 pipeline; phase90 numbering is a teaching model and internal passes can be repeated/reordered across releases.91 A successfully eliminated allocation has no allocation instruction in final machine code.92- C2's register allocator is **graph colouring, Chaitin-Briggs** (`opto/chaitin.cpp`). Linear93 scan is C1's technique. Do not describe C2 as linear scan.94- Strip mining supports safepoint polling in counted loops and interacts with loop optimization.95 On verified JDK 25.0.3, `UseCountedLoopSafepoints` is true for G1/ZGC/Shenandoah and false96 for Parallel/Serial — not one JDK-wide default.97- `DoEscapeAnalysis` has defaulted to `true` since JDK 6 Update 23 (~2010). Any material98 presenting it as a recent feature is out of date.99- Do not write a parser against `PrintEscapeAnalysis` / `PrintEliminateAllocations` output —100 it is internal compiler diagnostics and the exact strings vary between builds. Read it, then101 cross-check against the source of the method.102- In production, use allocation-rate/profile deltas as evidence, accounting for sampling,103 TLAB/outside-TLAB coverage, compilation state and workload. Absence of a sampled allocation104 cannot by itself confirm scalar replacement.105- A benchmark whose result is neither returned nor consumed by a `Blackhole` can have its whole106 body removed by dead code elimination. That is the default failure mode of any measurement in107 this area, not an edge case.108- Aggregate CPU overhead and p99 latency are different quantities. Never derive one from the109 other without an explicit queueing model.110111## Decision/validation checklist112113- If source, compilation identity or runtime evidence is missing, state the gap and the114 smallest capture that can resolve it. Keep the proposed cause conditional; do not115 recommend an inlining/threshold flag as a confirmed fix from source shape alone.116- Pin JDK vendor/update, compiler (C2 versus JVMCI), flags, compilation ID/level and profile117 maturity; reproduce after warm-up and after deoptimization/recompilation.118- State whether evidence is bytecode, ideal graph, compiler log, assembly, allocation sample or119 benchmark. Each can falsify different hypotheses and none substitutes for all others.120- Test semantic edge cases before refactoring for the compiler: exceptions, overflow, NaN,121 aliasing, concurrency/publication and uncommon paths can be the guards preventing an opt.122- Validate end-to-end throughput/tail/CPU/code-cache effects. A microbenchmark win under forced123 directives is not authorization for a process-wide production flag.124125Deliver the relevant compile ID and artifact location, the observed decision, the inferred126blocking mechanism, and one confirming or falsifying check. For a proposed change, include127the semantic constraints and before/after metric; say explicitly when it remains untested.128129## References130131- [C2 phases and the IR](references/c2-phases-and-ir.md) — the five tiers, the seven-phase132 pipeline, the three edge types of the sea-of-nodes graph, the inlining size limits and the133 three escape states, as tables. Read when you need to say _where_ in the pipeline a decision134 was made, or which limit a specific inlining verdict came from.135- [JIT diagnosis recipes](references/jit-diagnosis-recipes.md) — the exact flag combinations136 for tier, inlining and escape diagnosis, the factor-isolation runs, and the correct threshold137 tuning flags. Read when you are about to run the JVM to answer one of these questions.138139Authoritative sources: [OpenJDK C2 sources](https://github.com/openjdk/jdk/tree/master/src/hotspot/share/opto),140[HotSpot compiler control](https://docs.oracle.com/en/java/javase/25/vm/compiler-control.html),141and [JEP 165: Compiler Control](https://openjdk.org/jeps/165).