A
These are not four unrelated tools; they are points on a spectrum trading the same handful of quantities: startup latency, peak throughput, portability, and memory footprint. Understanding the spectrum is what lets you answer "why does Python start instantly but run slowly, while C is the reverse — and why does the JVM somehow get both?" The unifying idea underneath is the abstract machine.
Every language is defined against some abstract machine — an idealised computer whose instructions are the language's constructs. Executing a program means bridging the gap between that abstract machine and the real hardware. There are exactly two ways to bridge a gap, and every execution model is a mixture of them:
A pure compiler is all translation; a pure interpreter is all interpretation. Everything interesting — bytecode VMs, JITs — lives in between, translating part of the way (to a convenient intermediate abstract machine) and interpreting the rest, or deferring translation until run time when it knows more.
Line them up against the quantities they trade. Read each row as "what do I pay, and what do I get?"
| Model | When translated | Startup | Peak speed | Portability | Examples |
|---|---|---|---|---|---|
| AOT compiler | fully, before running | slow (build step) | high | low (one binary per target) | C, C++, Rust, Go |
| Pure interpreter | never — walks source/AST | instant | low | high (run anywhere) | classic shell, tiny BASIC |
| Bytecode + VM | to bytecode ahead of time; VM interprets | fast | medium | high (one bytecode, many VMs) | CPython, early JVM, Lua |
| JIT compiler | hot code, at run time, to native | fast, then warms up | very high | high (VM ships per target) | HotSpot JVM, V8, PyPy, .NET |
The bytecode-plus-VM row is why "compiled vs interpreted" is a false binary: javac is a
compiler (source → bytecode) and the JVM is an interpreter/JIT of that bytecode. Java is
both. The interesting question is never "compiled or interpreted?" but "translated how far, and
when?"
The tension is concrete. AOT pays a big fixed cost up front and then runs each unit of work cheaply. Pure interpretation pays nothing up front but a lot per unit forever. A JIT starts like an interpreter and bends toward the compiled cost as hot code gets compiled — so for a short run the interpreter wins, and for a long run the JIT wins. Model it and watch the crossover.
Run it: for a ten-unit script the interpreter wins outright (no build cost), for a huge batch job the AOT binary wins, and in the broad middle — long-running servers, browsers — the JIT is best, because it amortises a small, targeted compilation over a lot of work. That middle is where most software actually lives, which is why JITs power the JVM, JavaScript engines and .NET.
A production JIT such as HotSpot or V8 is profile-guided and tiered. It begins by interpreting, cheaply counting how often each method runs and each loop spins. When a counter crosses a threshold — the code is "hot" — it compiles that method to native code at a low optimisation tier, then, if it stays hot, recompiles it at a higher tier with aggressive optimisation. Crucially, it speculates using facts observed at run time ("this call site has only ever seen one type") and installs a cheap guard; if the guard ever fails, it deoptimises back to the interpreter and recompiles. This is the machinery that lets a dynamically-typed language reach speeds an AOT compiler, which cannot see run-time behaviour, often cannot match.
It sounds impossible — the AOT compiler has unlimited time and the whole program in front of it, while
the JIT must work in milliseconds during execution. The JIT's advantage is information the AOT
compiler can never have: what actually happens at run time. It sees that a supposedly
polymorphic call is monomorphic in practice, that a branch is taken 99.9% of the time, that a
final-in-effect value never changes, that a virtual dispatch always lands on one class. It
then speculatively compiles as if those facts were guaranteed — inlining across the virtual
call, straightening the hot path — guarded by a cheap check that bails out to the interpreter if a fact
is ever violated. The AOT compiler, forced to be correct for all possible runs, must keep the
general (slower) code. Profile-guided optimisation gives AOT a taste of this by feeding back a training
run, but the JIT gets the real run, every time, and can re-specialise if behaviour shifts.
This slogan fails in every direction. A naïve AOT compiler with no optimiser can emit worse code than a finely-tuned interpreter's inner loop. A JIT is a compiler that produces native code yet begins by interpreting — is it "compiled" or "interpreted"? Java is compiled to bytecode and interpreted and JIT-compiled, all in one run. And the modern truth is that a top JIT frequently beats an AOT binary on long-running workloads by exploiting run-time profiles the AOT compiler could not see.
The honest framing replaces the binary with the four quantities: startup latency, peak throughput, portability and memory/warmup cost. AOT trades slow startup and poor portability for fast, predictable steady-state; interpretation trades peak speed for instant start and portability; JIT chases both peaks at the price of warmup and a heavier runtime. "Fast" and "slow" are properties of a workload on a model, never of the word "compiled".