Detailed explanation
Just-In-Time (JIT) compilation is a runtime optimisation technique used by JVM implementations such as HotSpot. The Java compiler, javac, first converts source code into platform-independent bytecode stored in .class files. During execution, the JIT compiler translates selected bytecode into native machine instructions for the current processor. This reduces the repeated work of interpreting bytecode and allows frequently executed code to benefit from optimisations based on actual runtime behaviour.
In a typical HotSpot execution, methods initially run through the interpreter, while the JVM collects information such as invocation counts, loop activity, branch behaviour, and receiver types at method calls. Code that executes frequently is considered hot and becomes a compilation candidate. Compilation normally happens on background compiler threads, and generated machine code is stored in the JVM's code cache. Subsequent calls can use this compiled code. For a long-running loop, on-stack replacement (OSR) can transfer an already executing method into compiled code without waiting for its next invocation.
HotSpot commonly uses tiered compilation to balance startup speed and peak performance. Level 0 represents interpretation; levels 1–3 use the C1 compiler with different profiling configurations, while level 4 uses C2 for more aggressive optimisation. Code does not necessarily pass through every level in sequence. Optimisations can include method inlining, dead-code elimination, loop transformations, and allocation elimination when escape analysis supports it. Runtime profiles also enable speculative optimisation: the compiler may optimise for an observed receiver type while retaining checks. If an assumption becomes invalid, the JVM can deoptimise the affected execution and potentially recompile later.
JIT matters because Java performance changes as a program warms up. A loop executed millions of times may eventually run as optimised native code, but compilation itself consumes CPU time and code-cache memory. Short-lived programs may therefore see less benefit than long-running services. In interviews, distinguish javac from JIT, explain the profiling-and-compilation lifecycle, and avoid claiming that all bytecode is immediately compiled or that hotness depends on one universal execution threshold.
Key points to remember
- javac produces bytecode; the JIT compiler produces native machine code at runtime.
- HotSpot uses execution counters and runtime profiles to identify and optimise hot code.
- Tiered compilation combines interpretation, fast C1 compilation, and more aggressive C2 optimisation.
- OSR allows a running loop to switch into compiled code during the same method invocation.
- Warm-up, compilation costs, and deoptimisation affect observed application performance.
Common mistakes candidates make
- Confusing JIT compilation with javac compilation or claiming that JVM execution is purely interpreted.
- Saying every method follows a fixed compilation sequence after an identical number of executions.
- Timing a single cold run or an unused loop and treating it as reliable evidence of steady-state performance.
Likely follow-up questions
How do C1 and C2 differ?
C1 generally compiles quickly, providing early performance improvements and, at appropriate tiers, collecting profiling information. C2 spends more compilation effort on aggressive optimisations intended to improve peak performance.
What is deoptimisation?
Deoptimisation occurs when compiled execution can no longer safely rely on an optimisation assumption, such as an expected receiver type. HotSpot can reconstruct interpreter frames to continue correctly, and the method may later be recompiled using updated information.
How should you benchmark code affected by JIT compilation?
Use JMH with suitable warm-up iterations, measurement iterations, and separate JVM forks. Ensure results are consumed to prevent dead-code elimination, and measure startup separately if cold performance is your goal.
Interview tip: Explain the path from source code to bytecode to hot native code, then use a frequently executed loop to introduce tiered compilation and warm-up.
