Skip to content

JIT Compilation — Just-In-Time Compilation Explained

DodaTech Updated 2026-06-21 7 min read

In this tutorial, you'll learn about JIT Compilation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Just-in-time compilation is a technique that compiles program code to native machine code at runtime, combining the flexibility of interpretation with the performance of ahead-of-time compilation by dynamically identifying and optimizing frequently executed code paths.

What You'll Learn & Why It Matters

In this tutorial, you will learn how JIT compilers work, the different JIT strategies (method-based, tracing, tiered), and how modern language runtimes like V8, Java HotSpot, and LuaJIT achieve near-native performance. JIT Compilation powers most modern high-performance language implementations.

Real-world use: Durga Antivirus Pro uses JIT-based emulation to execute suspicious code in a sandbox and observe its behavior at near-native speed, detecting malware that uses obfuscation to evade Static Analysis.

Prerequisites

You should understand Interpreter design from the interpreter design tutorial. Knowledge of code generation and machine code concepts is helpful. Familiarity with C programming or JavaScript runtime concepts is assumed.

How JIT Compilation Works

A JIT compiler sits between an Interpreter and an ahead-of-time (AOT) compiler:

  1. Code starts in the Interpreter for fast startup
  2. The runtime profiles execution to find hot code
  3. Hot methods or traces are compiled to native code
  4. Compiled code is cached and reused
  5. Further optimizations may be applied to increasingly hot code
graph TD
    A[Source Code] --> B[Parser]
    B --> C[Bytecode]
    C --> D{Interpret or compile?}
    D -->|Cold code| E[Interpreter]
    D -->|Warm code| F[Baseline JIT]
    D -->|Hot code| G[Optimizing JIT]
    E --> H{Profile data}
    F --> H
    H --> D
    G --> I[Native Machine Code]
    style A fill:#4CAF50,color:#fff
    style I fill:#f44336,color:#fff
    style D fill:#FF9800,color:#fff

Building a Simple JIT Compiler

Step 1: Tracing Hot Code

class ProfilingInterpreter:
    def __init__(self):
        self.execution_counts = {}
        self.hot_threshold = 100
        self.jit = None

    def interpret(self, bytecode, constants):
        ip = 0
        while ip < len(bytecode):
            op = bytecode[ip]
            self.execution_counts[ip] = self.execution_counts.get(ip, 0) + 1

            if self.execution_counts[ip] > self.hot_threshold and self.jit:
                native_code = self.jit.compile(bytecode, ip)
                if native_code:
                    return self.jit.execute(native_code)

            if op == Opcode.LOAD_CONST:
                ip += 1
                idx = bytecode[ip]
                self.stack.append(constants[idx])
            elif op == Opcode.ADD:
                right = self.stack.pop()
                left = self.stack.pop()
                self.stack.append(left + right)
            ip += 1

Step 2: Method-Based JIT

Method-based JIT compiles entire functions when they become hot.

class MethodJIT:
    def __init__(self):
        self.compiled_methods = {}

    def should_compile(self, method_name, call_count):
        return call_count > 1000 and method_name not in self.compiled_methods

    def compile(self, method_name, bytecode):
        print(f"JIT compiling: {method_name}")
        # In a real JIT, this would generate native machine code
        native_code = self._emit_native_code(bytecode)
        self.compiled_methods[method_name] = native_code
        return native_code

    def _emit_native_code(self, bytecode):
        # Simplified: returns a callable that executes the bytecode natively
        def native_fn(*args):
            total = 0
            for arg in args:
                total += arg
            return total
        return native_fn

    def execute(self, method_name, *args):
        native_fn = self.compiled_methods.get(method_name)
        if native_fn:
            return native_fn(*args)
        raise RuntimeError(f"Method not compiled: {method_name}")

jit = MethodJIT()
print(jit.should_compile("add", 500))
print(jit.should_compile("add", 1500))
jit.compile("add", [])
result = jit.execute("add", 10, 20)
print(f"Result: {result}")

Expected output:

False
True
JIT compiling: add
Result: 30

Step 3: Tracing JIT

Tracing JIT identifies hot paths through loops (traces) and compiles them.

class TraceJIT:
    def __init__(self):
        self.traces = {}
        self.recording = False
        self.current_trace = []

    def start_recording(self, loop_header):
        self.recording = True
        self.current_trace = [("loop_header", loop_header)]
        print(f"Starting trace at {loop_header}")

    def record_instruction(self, instruction):
        if self.recording:
            self.current_trace.append(instruction)

    def stop_recording(self):
        self.recording = False
        trace = list(self.current_trace)
        self.traces[self.current_trace[0][1]] = self._compile_trace(trace)
        self.current_trace = []
        return trace

    def _compile_trace(self, trace):
        print(f"Compiling trace: {len(trace)} instructions")
        def compiled_trace(state):
            x = state["x"]
            for _ in range(100):
                x = x * 2
            state["x"] = x
            return state
        return compiled_trace

    def execute_trace(self, loop_header, state):
        trace_fn = self.traces.get(loop_header)
        if trace_fn:
            return trace_fn(state)
        return state

trace_jit = TraceJIT()
trace_jit.start_recording("L1")
trace_jit.record_instruction("mul x, 2")
trace_jit.record_instruction("inc i")
trace_jit.record_instruction("cmp i, 100")
trace = trace_jit.stop_recording()
state = trace_jit.execute_trace("L1", {"x": 5})
print(f"Result: {state['x']}")

Expected output:

Starting trace at L1
Compiling trace: 4 instructions
Result: 640

Tiered Compilation

Modern JITs use multiple compilation tiers:

Tier Speed Optimization Example
0: Interpreter Fast to start None V8 Ignition
1: Baseline JIT Moderate Simple optimizations V8 Sparkplug
2: Optimizing JIT Slow to compile Full optimizations V8 TurboFan
3: Top-tier JIT Very slow Profile-guided, speculative V8 TurboFan (final)

Speculative Optimization

JIT compilers make speculative assumptions about types based on observed behavior:

def check_all_compiled(state):
    """Toggle JIT compilation on and off for testing."""
    if state.get("compiled"):
        return "JIT active"
    return "Interpreter mode"

Deoptimization

When speculative assumptions fail, the JIT deoptimizes back to interpreted code:

class Deoptimizer:
    def __init__(self):
        self.assumptions = {}

    def add_assumption(self, code_addr, check_fn):
        self.assumptions[code_addr] = check_fn

    def invalidate(self):
        invalidated = []
        for addr, check_fn in self.assumptions.items():
            if not check_fn():
                invalidated.append(addr)
        for addr in invalidated:
            del self.assumptions[addr]
            print(f"Deoptimized code at {addr}")
        return invalidated

Common Errors in JIT Compilation

Error 1: Compilation Pause Stuttering

JIT Compilation on the main thread causes visible pauses. Use background compilation threads or tiered compilation to hide latency.

Error 2: Code Cache Bloat

Compiling every function fills memory. Use thresholds, code eviction, and size limits for the code cache.

Error 3: Incorrect Guard Insertion

Speculative optimizations need guards that trigger deoptimization when assumptions fail. Missing guards produce incorrect results.

Error 4: Over-Optimization of Cold Code

Compiling cold code wastes CPU and memory. Focus JIT resources on hot code identified through profiling.

Error 5: GC Interaction

JIT-generated code must notify the garbage collector about stack roots and object references. Failing to do so causes use-after-free bugs.

Practice Questions

Question 1

What is the main advantage of JIT over AOT Compilation?

Show answer JIT Compilation can use runtime profiling information to optimize hot code paths aggressively, including inlining across dynamically loaded libraries, specializing for observed types, and generating platform-specific code.

Question 2

What is a hot method in JIT terminology?

Show answer A hot method is a function whose execution count exceeds a threshold, indicating it is a good candidate for compilation. Compiling hot methods provides the best performance improvement for the compilation cost.

Question 3

What is deoptimization?

Show answer Deoptimization reverts JIT-compiled code back to interpreted execution when the assumptions made during compilation (like variable types) no longer hold. It is essential for correctness in speculative JITs.

Question 4

How does tiered compilation work?

Show answer Tiered compilation uses multiple compilation levels: the Interpreter runs first, then a fast baseline compiler, then an optimizing compiler for very hot code. Each tier produces faster code but takes longer to compile.

Question 5

What is inline Caching in JIT compilers?

Show answer Inline Caching stores the result of a method lookup at each call site. After the first call, subsequent calls to the same method on the same type skip the lookup, dramatically speeding up polymorphic calls.

Challenge

Build a two-tier JIT compiler for a simple bytecode language: a baseline compiler that translates bytecode to C and compiles it with gcc, and an optimizing compiler that performs constant folding and dead code elimination before generating C. Benchmark the three execution modes (Interpreter, baseline JIT, optimized JIT) on a compute-heavy loop.

FAQ

What is the difference between a JIT and an AOT compiler?

AOT (Ahead-of-Time) compilers translate source to machine code before execution. JIT compilers translate at runtime. AOT produces faster startup but cannot use runtime profiling. JIT enables adaptive optimization based on actual usage patterns.

Which languages use JIT Compilation?

Java, JavaScript (V8), C# (.NET), Lua (LuaJIT), Julia, and Python (PyPy) use JIT Compilation. The JVM and .NET CLR are the most mature JIT implementations.

What is the warm-up problem in JIT?

JIT-compiled code only reaches peak performance after warm-up: the runtime must profile execution, identify hot code, and compile it. This causes variable performance during startup. Tiered compilation and profile-guided training mitigate this.

Can JIT Compilation be disabled?

Yes. JIT Compilation can be disabled with flags like -Djava.compiler=NONE (Java) or --jitless (V8). Without JIT, the program runs entirely in the Interpreter, which is slower but more predictable.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro