Java JVM JIT Bytecode Compilation Interpreted Tiered Compilation HotSpot

Is Java Really a Compiled Language?

Aliaksei Kankou Aliaksei Kankou
· July 10, 2024 · 6 min read

Java is commonly labeled a compiled language — but that is only half the story. Java is neither a purely compiled nor a purely interpreted language. Instead, Java employs a hybrid approach that combines compilation, interpretation, and tiered Just-In-Time (JIT) compilation. Unlike C, which compiles directly to native machine code, Java is first compiled to platform-independent bytecode. At runtime, the JVM starts by interpreting that bytecode and progressively compiles hot code to native machine code using two JIT compilers: C1 and C2.

Java's Compilation and Execution Process

JVM Tiered Compilation Pipeline — Interpreter, C1 JIT, C2 JIT, Code Cache
JVM runtime execution pipeline: from bytecode to native machine code via interpreter and two-tier JIT compilation.
1

Writing Java Code

  • Developers write Java source code in .java files.
2

Compilation to Bytecode

  • The Java Compiler (javac) translates .java files into bytecode, stored in .class files. This bytecode is a compact, platform-independent intermediate representation — it is not native machine code and cannot run directly on the CPU.
3

Class Loading

  • At runtime, the JVM's Class Loader reads the .class files and loads the bytecode into memory, making it available for execution.
4

Interpreter — Cold Code Execution

  • All bytecode initially runs through the Interpreter. It executes bytecode instructions one at a time, directly and without emitting any native code of its own.
  • The interpreter is fast to start but slower to execute, as it re-processes each instruction every time it is encountered.
  • While interpreting, the JVM collects profiling data — invocation counts and type feedback — to identify code worth compiling.
5

C1 JIT Compiler — Warm Code

  • When a method is invoked frequently enough to be considered warm, the JVM compiles it with the C1 (client) JIT compiler.
  • C1 applies moderate optimizations such as method inlining and basic loop transformations. It compiles quickly, trading peak performance for fast compilation.
  • The resulting native code is stored in the Code Cache and reused on subsequent calls, eliminating interpreter overhead.
6

C2 JIT Compiler — Hot Code

  • For methods that remain heavily invoked after C1 compilation, the JVM promotes them to the C2 (server) JIT compiler.
  • C2 performs deep, aggressive optimizations — escape analysis, loop unrolling, speculative type narrowing — to produce the fastest possible native code.
  • C2 takes longer to compile than C1, but the output is significantly faster for code that runs millions of times.
7

Deoptimization

  • Both C1 and C2 make speculative optimizations based on observed runtime behavior. If those assumptions are later violated — for example, a method that always received integers suddenly receives a string — the compiled code is invalidated.
  • Deoptimization always unwinds back to the interpreter (bytecode level), never directly between JIT tiers. The JVM then re-profiles the method and may recompile it through C1 and C2 again.
8

Code Cache

  • Compiled native code from both C1 and C2 is stored in the Code Cache — a dedicated memory region inside the JVM. This allows compiled methods to be reused without recompilation on subsequent invocations.
9

Execution by the OS and CPU

  • The native code from the Code Cache is executed directly by the CPU via the operating system. The JVM manages memory, garbage collection, and system interactions so the program logic can run efficiently on any platform.

So, Is Java Compiled or Interpreted?

The answer is: both, and more. Java source code is compiled to bytecode at build time. At runtime, the JVM always starts by interpreting that bytecode — there is no ahead-of-time native compilation step. As methods accumulate invocations, the JVM progressively promotes them: first to C1 (moderately optimized native code), then to C2 (aggressively optimized native code). This tiered strategy lets Java start quickly and reach near-native performance for frequently executed code.

Conclusion

Java's execution model is a carefully engineered pipeline. The Java compiler produces portable bytecode; the JVM's interpreter runs it immediately with zero warmup cost; C1 eliminates interpreter overhead for warm methods; and C2 squeezes out maximum throughput for hot code. Deoptimization ensures correctness whenever speculative assumptions break down. This hybrid approach is why Java delivers both platform independence and competitive performance — making it one of the most widely used languages in production systems worldwide.

What this post does not cover

This post focuses on the standard HotSpot JVM execution model. Ahead-of-time (AOT) compilation — including GraalVM Native Image and Project Leyden — is outside the scope of this article.

Aliaksei Kankou

Aliaksei Kankou

Lead Full-Stack Engineer and Cloud Architect with a Bachelor's degree in Software Engineering and 15+ years of experience.