GraalVM JIT versus C2
Purpose
Decide whether replacing C2 with Graal is worth it for a specific workload, and reach that decision from a measurement rather than from a reputation. Graal substitutes only the tier-4 compiler — interpreter, C1, GC and threading remain HotSpot's — so the entire question narrows to whether Graal's optimisations pay off for this code, on a distribution that actually ships the compiler and will keep shipping it.
The failure this prevents is the comparison that measures something other than what it claims. Three versions of it dominate: warm-up not controlled, so the sample mixes interpreter, C1 and tier 4 and Graal looks slow while still climbing; the run landing in jargraal mode, where the compiler itself is being compiled by C1 and that cost is attributed to "Graal's deeper analysis"; and a prior about which workloads favour which compiler carried across compiler versions that changed the answer.
Workflow
Inspect the project's toolchain, runtime image, exact distribution/build, flags and CPU/ memory limits first. The examples here target HotSpot/GraalVM JDK 25; they do not authorize upgrading an older project or changing its distribution. Return the observed compiler/mode, comparison conditions and result with uncertainty, then a conditional recommendation and the measurement that would confirm it. Missing profiles or compiler evidence mean the migration benefit remains unproven.
- Establish which product is under discussion. GraalVM JIT compiles at runtime with a JVM present, optimising peak throughput at the cost of start-up. Native image compiles at build time with its own runtime rather than HotSpot, targeting faster start-up at the cost of adaptive optimisation. Opposite trade-offs; treating them as one thing produces migration mistakes in both directions.
- Establish where Graal can even come from for this JDK. In 2026 that is a GraalVM
distribution — GraalVM Community or Oracle GraalVM, both on a JDK 25 base through the
25.x line. A stock OpenJDK has JVMCI but no compiler:
-XX:+UseJVMCICompilerpasses-versionsilently and dies at the first compilation withCannot use JVMCI compiler: No JVMCI compiler found. Oracle JDK 25 removed the Graal JIT that 23 and 24 bundled. Seereferences/troubleshooting-and-timeline.md. - Confirm the mode and the configuration before interpreting any warm-up number.
-Djdk.graal.ShowConfiguration=infoprints one line:loaded from a Native Image shared libraryis libgraal,loaded from class filesis jargraal. The JFRjdk.Compilationevent'scompilerfield (jvmciversusc2) proves who produced the tier-4 code — HotSpot's-XX:+PrintCompilationnever names the compiler. - Profile first, then benchmark the methods profiling actually named. Build JMH benchmarks for the real hot paths, not a synthetic microbenchmark detached from the application's allocation pattern.
- A/B inside one binary.
-XX:-UseJVMCICompileron the GraalVM build gives C2 on the same class library, the same GC build and the same machine, so the compiler is the only intended treatment. Pin the GC and all non-compiler flags on both runs. Use independent forks, inspect per-iteration convergence, retain confidence intervals/raw results, and compare both steady state and time-to-steady-state; a fixed “last iterations within 5%” rule can accept drift or reject normal noise. - Read the result against the workload shape and the compiler version, not against
expectation. GraalVM Community 25.3 gained loop vectorisation and a new default inliner;
a prior formed on 25.0 is stale. See
references/workload-fit-and-migration.md. - Check the gate conditions before migrating: measured lifetime break-even, consistent wins across runs, native image considered if the critical metric is start-up, the licence confirmed at the official source, and a support horizon for the GraalVM line now that it is detached from the Java SE release train.
- After migrating, confirm the laboratory gain in production under real load, with a
tested rollback plan, and watch RSS: libgraal's threads and isolate heap live outside
-Xmx.
Rules
- GraalVM JIT replaces the tier-4 compiler only. On GraalVM CE 25.0.2
TieredCompilation,TieredStopAtLevel, theTier3/Tier4thresholds and the G1 default are byte-for-byte HotSpot's in that build. Replacing the compiler does not replace those subsystems, but changed allocation, generated code and compiler CPU usage can change observed GC, scheduling and tier progression. Recheck target-build ergonomics and collector support. - JVMCI (JEP 243, JDK 9) is the interface that makes this possible, and it survived
JEP 410. JEP 410 (JDK 17) removed
jdk.aot,jdk.internal.vm.compilerand its management module from OpenJDK and keptjdk.internal.vm.ci. JDK 22 onwards carries an emptyjdk.graal.compilerplaceholder (JDK-8318027, Galahad preparation) and the-XX:+UseGraalJITflag; on Temurin 25.0.3 the flag fails withJVMCI compiler 'graal' specified by jvmci.Compiler not found. Galahad was dissolved in March 2026. Using Graal means a GraalVM distribution, not a flag on OpenJDK. - On a stock JDK the JVMCI flags are
{JVMCI experimental}and need-XX:+UnlockExperimentalVMOptionsfirst; on GraalVM they are{JVMCI product}and need nothing. The stock-JDK failure is late — at the first compile request, not at start-up.-XX:+BootstrapJVMCIcan expose it in an isolated preflight; it adds compilation work and is not a mandatory production launch flag. A representative hot-code smoke test with compiler evidence is another validation route. - Graal options use the
-Djdk.graal.prefix (GraalVM for JDK 22 onwards).-Dgraal.still works with a deprecation warning on 25.0 and without one on 25.1. An option name Graal does not know is fatal at start-up —Error parsing Graal options: Could not find option X— so never carry a flag across versions unlisted:-XX:+JVMCIPrintPropertieswith-Djdk.graal.PrintPropertiesAll=trueis the authoritative list for the build in use. - There is no
CompilerThreadsGraal option. Compiler threads are-XX:CICompilerCountsplit by-XX:JVMCINativeLibraryThreadFraction(0.66 since GraalVM for JDK 24, JDK-8337493): a lower fraction trades warm-up for lower peak RSS. - Partial escape analysis is the central conceptual advantage. C2's escape analysis is flow-insensitive for this decision: an object classified as escaping because of one path is not scalar-replaced only on the non-escaping path. Graal PEA can keep it virtual across paths and materialise it where required. Inlining, identity use, synchronization, array limits and compiler heuristics still constrain both compilers; prove the allocation change.
- Cite PEA correctly: Lukas Stadler, Thomas Würthinger, Hanspeter Mössenböck, CGO 2014. It is not Christian Wimmer, PLDI 2013 — that attribution is a common error.
- Graal's inlining budget is measured in graph nodes (
TrivialInliningSize=10,MaximumInliningSize=300,SmallCompiledLowLevelGraphSize=330on CE 25.0.2), not in bytecode bytes like C2'sMaxInlineSize/FreqInlineSize. The numbers do not transfer, and-XX:+PrintInliningshows nothing for Graal's tier-4 decisions — use-Djdk.graal.TraceInlining=truewith-Djdk.graal.MethodFilter. GraalVM 25.3 replaced the inliner (UsePriorityInlining), so 25.0 traces do not describe 25.3. - libgraal and jargraal are not the same configuration. libgraal is the compiler compiled
ahead of time into
libjvmcicompiler, so it pays no warm-up of its own. jargraal (-XX:-UseJVMCINativeLibrary) runs as bytecode and is compiled by C1 only (CompileGraalWithC1Only=true), so its penalty is per compilation for the life of the process while new compilations occur. That does not imply a permanent application slowdown: once hot code is installed, compilation costs can be amortised. Measure compilation CPU and the complete workload lifetime separately. The(gc=Serial GC)in the libgraal configuration line is the compiler isolate's own GC, not the application's. - Compiler configurations are
communityandeconomyon CE;enterpriseexists only on Oracle GraalVM. Requesting one the build lacks does not fall back — JVMCI is disabled after N failed upcalls and the JVM aborts with anhs_errfile. A flags file copied from Oracle GraalVM onto CE is the usual way to hit this. - No percentage gain counts as a fact without the source, the version and the workload.
"Numeric loops favour C2" was true of CE through 25.2, which had no auto-vectorisation
(Oracle-only
Vectorization); CE 25.3 enablesVectorizeLoopsby default. Reproduce on the exact GraalVM line before it informs a migration decision. - Short-lived workloads (functions, CLIs, jobs measured in seconds) are weak candidates when added compilation cost exceeds the runtime saving. Measure cold start, warm-up CPU, total job duration and invocation reuse; C2, HotSpot AOT caches, CRaC or native image are competing choices with different compatibility and operational costs.
- Do not compare each side under different default GC or security/compiler option sets. Pin
non-treatment flags, record ergonomically selected values, and include options such as
jdk.graal.SpectrePHTBarriersin the equivalence review: changing a mitigation changes both security posture and generated-code cost. - The two distributions are Oracle GraalVM (GFTC, Oracle JDK base) and GraalVM CE (GPLv2 with Classpath Exception, OpenJDK base). Since September 2025 GraalVM is detached from the Java SE release train: Oracle JDK 24 was the last Oracle JDK with a bundled Graal JIT, GraalVM 25.x ships monthly innovation releases on a JDK 25 base. The dissolved Galahad project is no delivery commitment for future OpenJDK integration. Licence and support horizon are both gates; JDK 17 CPU releases already moved from GFTC to the OTN licence. Confirm at the official source before a corporate decision.
- Truffle languages (GraalJS, GraalPy) need the Graal compiler for partial evaluation. On
OpenJDK or Oracle JDK 25 the optimising runtime exists only as a polyglot isolate;
otherwise the engine prints
[engine] WARNING: The polyglot engine uses a fallback runtimeand interprets. Since GraalVM 25.1 jargraal on plain OpenJDK is unsupported for Truffle. Embedding a Graal language is a reason to run on GraalVM independent of the C2-versus-Graal throughput question.
References
The release-line facts below were checked against GraalVM 25.3.4.1 (2026-08-25). Consult the current official release calendar and target build properties before a new migration.
- Workload fit and the migration decision — the workload-shape table with the reason each way and the GraalVM line it was true on, the strong and weak candidate profiles, the gate checklist, and the 2026 licensing and support picture. Read when deciding whether to evaluate or adopt Graal for a given service.
- Enabling and comparing Graal — the activation
paths and what each really yields, confirming libgraal versus jargraal and who compiled
tier 4, the verified
-Djdk.graal.option set, and the single-binary JMH comparison. Read before running the comparison. - Troubleshooting and timeline — the symptom-to-cause table for Graal that will not start, silently is not running, crashes or underperforms, production behaviour at scale (threads, RSS, code cache), the Truffle runtime matrix, and the JEP and release timeline from JVMCI to the detachment. Read when a Graal run misbehaves or when a claim about "Graal in OpenJDK" needs a date.