Interpreter vs Compiler
On this page 12
Part I — Foundations · Interview reference
Execution model literacy: how source becomes machine work, where time goes (parse, compile, warmup, runtime), and how that drives language/runtime choices in production systems.
Definitions
| Model | Definition |
|---|---|
| Compiler (AOT) | Translates source (or IR) to machine code ahead of time → native binary |
| Interpreter | Executes source or bytecode instruction-by-instruction without producing a standalone native binary for the whole program |
| Bytecode + VM | Hybrid: compile source → portable bytecode; VM interprets and/or JITs |
| JIT | Compiles hot paths to native code at runtime using profiling data |
| AOT (modern) | e.g. Graal native-image, .NET Native, Go — compile ahead, often with different optimization limits than JIT |
Most production languages are hybrids, not pure textbook interpreter or compiler.
Comparison
| Dimension | Classic interpreter | AOT native | Bytecode + JIT (JVM/CLR/V8) |
|---|---|---|---|
| Startup | Fast to begin executing | Fast once binary loaded | Slower (classload + warmup) |
| Peak throughput | Lower | High | Often highest (PGO-like) |
| Portability | High | Per-arch binaries | High bytecode; JIT per-arch |
| Observability / rewrite | Easy eval / REPL | Harder | Agent / instrumentation rich |
| Optimization info | Limited | Static | Speculative + deoptimize |
| Binary size / deps | Small runtime | Larger or static | Large runtime (JVM) |
Pipeline mental model
Source → Parse/AST → (Optional bytecode/IR) → Optimize → Execute
↓
Interpreter loop and/or JIT tiers
↓
Deoptimization / recompilation (JIT systems)
Key JIT ideas
- Interpretation / C1 first for quick startup
- Hotspot detection → higher optimization tiers (C2 / Graal)
- Speculative opts (type feedback, inlining) + deopt if assumptions fail
- Inline caches for dynamic dispatch (JS, and polymorphic call sites on JVM)
Language examples (map correctly in interviews)
| Language | Model (simplified) |
|---|---|
| C/C++/Rust/Go | AOT to native (Go has GC runtime) |
| Java / Kotlin / Scala | javac → bytecode → JVM interpret/JIT (AOT optional) |
| C# | IL → CLR JIT (AOT options exist) |
| JavaScript | Parse → bytecode → JIT tiers (V8 Ignition/TurboFan etc.) |
| Python (CPython) | Bytecode interpreted; GIL-bound; alternate runtimes (PyPy JIT) |
| SQL | Query compiled/planned to execution operators (not a general language VM story, but “compiler” vocabulary appears) |
Java under the hood
.java → javac → .class bytecode → class load/verify
→ template interpreter (cold)
→ C1 (client) JIT → C2 / Graal (server) on hot methods
→ deoptimize if speculative assumptions fail
| Piece | Role |
|---|---|
| Bytecode | Stack-based ops; portable; verified before run |
| Tiered compilation | Interpret / C1 first for startup; C2 for peak throughput |
| Profiling | Type feedback, branch frequencies drive inlining & speculative opts |
invokedynamic | Lambdas, string concat, dynamic languages — call-site linkage |
| Graal native-image | Closed-world AOT: fast start, smaller footprint; limited dynamic features vs JIT peak |
| Warmup | First requests pay classload + JIT; measure after soak (JMH, realistic traffic) |
“Is Java compiled or interpreted?” → both: AOT to bytecode, then interpret and/or JIT (optional AOT native). Details in JVM.
Production case study (high volume)
Context: Payments API on HotSpot (~tens of K RPS per cluster) vs a latency-sensitive Lambda/Fargate sidecar compiled with Graal native-image for auth policy checks. Why seniors care: Post-deploy p99 is dominated by warmup (classload + C2), not “Java is slow”; native-image trades peak throughput / peak opts for cold-start and footprint; wrong choice fails SLOs differently. Failure / symptom: Canary looks bad for 5–15 minutes then fine; Lambda p99 timeouts on cold paths; megamorphic call sites after a plugin refactor never reach C2 peak. Resolution: Soak before declaring regression; gradual traffic / pre-warm; JMH + production mirrors; reserve AOT for cold-start budgets; keep long-lived services on JIT unless ops constraints win. Seen at / similar to: Shopify/Netflix JVM warmup discussions; AWS Lambda SnapStart / Graal; Twitter historical JVM tuning blogs.
Tradeoffs seniors must articulate
Choose AOT / native when
- Cold start and predictable latency matter (CLI, serverless tight budgets, sidecars)
- Deploy size / no JVM ops preferred
- Hard real-time-ish constraints (with care)
Choose JIT VM when
- Long-running services where warmup amortizes
- Need rich tooling, GC, dynamic loading, hotswap/agents
- Peak throughput and adaptive opts matter
Choose interpreted / dynamic when
- Gluing, scripting, data science iteration speed
- Accept throughput and packaging tradeoffs (or use specialized runtimes)
Performance & operations angles
- Warmup: JIT systems show rising performance after load; benchmarks must account for this (JMH, realistic soak)
- Deoptimization storms: changing shapes / megamorphic call sites
- Debug vs release: assertions, bounds checks, inlining differences
- Polyglot / FFI: crossing runtimes kills inlining and adds copy costs
Production case study (high volume)
Context: Ad-tech JVM service embeds a scripting/rules engine and calls into native crypto via JNI for every bid request (hundreds of K RPS). Why seniors care: Deoptimization storms and JNI transitions destroy inlining; “faster native” can raise p99; profile-guided reality beats theory. Failure / symptom: Code cache churn; JFR shows deopt; CPU in JNI transition; throughput drop after adding “flexible” dynamic proxies on the hot path. Resolution: Stabilize call shapes; move rare dynamic paths off the bid hot path; prefer pure-Java crypto where competitive; track deopt counts and code-cache occupancy. Seen at / similar to: High-QPS ad exchanges; Android ART/JIT history as analogy; V8 megamorphic IC stories applied to JVM polymorphic sites.
What interviewers probe
- “Is Java compiled or interpreted?” — require the layered answer (bytecode + JIT + possible AOT).
- JIT vs AOT tradeoffs for a microservice vs a CLI tool.
- Why PyPy or V8 can beat naive interpreters — hot paths + type specialization.
- Warmup and tail latency after deploy / scale-out.
- How reflection / dynamic proxies affect JIT (JVM link).
- Deopt / code-cache symptoms under production traffic shapes.
Senior-level expectation: Accurate hybrid model; connect runtime choice to SLOs (startup, p99, memory, ops skillset).
Pitfalls
- Calling the JVM “just an interpreter”
- Microbenchmarks without warmup or with dead-code elimination artifacts
- Assuming AOT always faster than JIT peak
- Ignoring binary portability and CVE patch story (static Go vs shared libc, JVM patch cadence)
- Declaring a deploy “regressed” during JIT warmup windows
- Putting megamorphic plugins on the highest-QPS path without measuring deopts