「VM」這個詞的兩種用法#
容易混淆 ── 中文都翻「虛擬機器」,英文都是 VM,但根本不是同一回事:
| 類別 | 例子 | 模擬什麼 |
|---|---|---|
| System VM | KVM、VMware、VirtualBox | 整顆機器(x86、ARM) |
| Process VM(語言 VM) | JVM、CPython、V8、CLR、mruby | 一個語言的執行環境 |
本章只談語言 VM ── 它執行的不是 x86 指令,是某個語言設計的 bytecode(或 AST)。
System VM 請見 Linux 核心內部 / 虛擬化與容器。
為什麼語言要用 VM#
不用 VM 的選項是「直接編成原生機器碼」(C、Rust、Go)── 跑得快、不需要 runtime。但這放棄了一些東西:
| 用 VM 換到的好處 | 機制 |
|---|---|
| 可攜性 | bytecode 跨平台,VM 自己處理底層差異 |
| 動態語意 | eval、reflection、monkey-patch、duck typing |
| 安全沙箱 | VM 控制可呼叫的 API(瀏覽器、嵌入式腳本) |
| 易實作 | 寫 bytecode 派發比寫 codegen 容易十倍 |
| 自動記憶體管理 | GC 與 VM 緊密合作 |
| 動態優化 | JIT 看到實際型別與熱路徑後再生成程式碼 |
代價:
- 直譯開銷(即使有 JIT,warmup 階段慢)
- 啟動時間(要載入 VM 本身)
- 記憶體(VM + GC 的常駐 footprint)
三種主流形式#
Tree-walking interpreter#
把原始碼解析成 AST,直接遞迴走訪:
source code → lexer → parser → AST → interpreter(直接 walk 樹)最簡單,但每次執行都要走樹結構(節點 dispatch + 子節點 vtable),慢。Ruby 1.8、早期 Python、教科書範例都用這種。
Bytecode VM#
把 AST 進一步編譯成緊湊的指令序列(bytecode),用迴圈一條一條執行:
source code → AST → bytecode compiler → bytecode → VM dispatch loop比 tree-walker 快幾倍到十幾倍。Python(CPython)、Ruby(YARV)、Lua、JVM 直譯模式、CLR、mruby、Erlang BEAM。
+----------+
| AST |
+----+-----+
│ compile
▼
+--------------------+
| bytecode (binary) | ← 緊湊、線性、易快速 dispatch
+--------------------+
│ VM 載入
▼
+--------------------+
| dispatch loop |
+--------------------+JIT#
執行期把 bytecode 編譯成機器碼,跳過直譯:
bytecode → JIT compiler → 原生機器碼 → CPU 直接執行JVM HotSpot、V8、JavaScriptCore、PyPy、LuaJIT、SpiderMonkey、CRuby YJIT。
JIT 不取代 bytecode VM,是疊在它之上:冷程式碼走直譯,熱程式碼編成原生碼。Warmup 結束後效能可逼近 C。
兩種 bytecode VM:stack vs register#
最古老的分歧。差別在於 opcode 如何指定運算元。
Stack-based#
每個 opcode 從堆疊頂端取運算元、結果壓回堆疊。
;; a = b + c (假設 b, c 在 local slot 1, 2)
LOAD_LOCAL 1 ;; 推 b 上 stack
LOAD_LOCAL 2 ;; 推 c 上 stack
ADD ;; pop 兩個、加總、push 結果
STORE_LOCAL 0 ;; 存到 aVM 內部維護 operand stack。代表:JVM、CPython、CLR、Lua、Wasm。
優點:
- 編碼緊湊(opcode 不需要寫運算元,一個 byte 即可)
- bytecode compiler 容易實作(直接從 AST post-order 遍歷)
- 跨平台(不需要選 register 數量)
缺點:
- 同一個值要算多次或讓給多個 opcode 用,要
DUP多次 - 直譯開銷較大:dispatch 次數多
Register-based#
每個 opcode 直接指定虛擬暫存器索引:
;; a = b + c
ADD R0, R1, R2 ;; R0 := R1 + R2代表:Lua 5.0+、Dalvik(Android 早期)、mruby、V8 Ignition。
優點:
- 同樣語意所需 opcode 數較少(dispatch 次數少 30~50%)
- 容易做 peephole 優化、register allocation
缺點:
- 每個 opcode 要 encode register index,bytecode 較大
- compiler 要做 register allocation
研究顯示 register VM 整體效能略勝(單純直譯、無 JIT 時)。但加上 JIT 後差異變小,因為 JIT 會把這些抽象都優化掉。
bytecode VM 的整體架構#
不論 stack 或 register,主流 VM 結構大同小異:
+-------------------------------+
| Source code |
+--------------+----------------+
│
┌───────▼────────┐
│ Lexer / Parser │ (前端,編譯期)
└───────┬────────┘
│
┌──────▼──────┐
│ AST │
└──────┬──────┘
│
┌──────▼──────────┐
│ Bytecode emitter│
└──────┬──────────┘
│
┌──────▼──────┐
│ Bytecode │ ← 持久化、可 dump 為檔案
└──────┬──────┘
│ load / mmap
+──────────────▼────────────────+
│ VM Runtime │
│ ┌────────────────────────┐ │
│ │ Loader / Linker │ │
│ ├────────────────────────┤ │
│ │ Dispatch Loop │ │
│ │ fetch → decode → exec │ │
│ ├────────────────────────┤ │
│ │ Value Representation │ │
│ │ (Tagged union / NaN-box)│ │
│ ├────────────────────────┤ │
│ │ Heap & GC │ │
│ ├────────────────────────┤ │
│ │ Stack / Frames │ │
│ ├────────────────────────┤ │
│ │ Object model │ │
│ │ (Class, method table) │ │
│ ├────────────────────────┤ │
│ │ Native API / FFI │ │
│ └────────────────────────┘ │
+───────────────────────────────+每個元件後續章節會展開:
- 02 整體架構與資料流
- 03 dispatch loop 細節
- 04 value 表示
- 06 frame 設計
- 08–09 物件模型
- 10 GC
bytecode 不是組合語言#
許多人第一次看 bytecode 會想到 x86 asm,但兩者本質不同:
| 特徵 | x86 機器碼 | bytecode |
|---|---|---|
| 抽象層級 | 直接對應 CPU | 對應語言抽象(method、object) |
| 動態性 | 靜態 | 通常含動態 dispatch(method lookup) |
| 變數型別 | 無(只有 byte / word) | 通常仍有型別資訊 |
| 記憶體 | 手動 | 通常自動(GC) |
| 例外 | 透過 OS / 約定 | 內建語意 |
bytecode 的設計本身就反映語言設計:JVM bytecode 假設「物件是 reference、方法 dispatch 是動態」、Wasm bytecode 假設「結構化控制流、明確型別」、mruby IREP 假設「Ruby block、symbol 表」。
編譯期 vs 執行期的工作分配#
每個語言要決定每件事在哪做:
| 工作 | 編譯期做 | 執行期做 |
|---|---|---|
| 詞法 / 語法 | 永遠 | (eval 時才會) |
| 型別檢查 | Java、C# 部分 | Python、Ruby、JavaScript |
| Method lookup | C++ 靜態 dispatch | 動態語言 / Java virtual call |
| 常量摺疊 | 多數做 | - |
| Inlining | AOT 編譯器 | JIT |
| 死碼消除 | AOT 與 JIT | - |
「動態語言」其實就是「把更多工作推到執行期」。這給程式設計師彈性,給 VM 工程師頭痛 ── 而 JIT 就是把執行期觀察到的資訊回流到編譯,把優化重新拿回來。
一些值得知道的 VM#
| VM | 語言 | 特色 |
|---|---|---|
| JVM HotSpot | Java/Kotlin | tiered JIT、世代式 GC、極度成熟 |
| CPython | Python | 簡單 stack VM、ref counting + GC |
| V8 | JavaScript | Ignition (bytecode) + TurboFan (JIT) |
| JavaScriptCore | JavaScript | LLInt / Baseline / DFG / FTL 四階層 |
| Lua / LuaJIT | Lua | 極簡 register VM;LuaJIT 是 tracing JIT |
| BEAM | Erlang/Elixir | 為大量併發 + 容錯設計 |
| Wasm runtime | Wasm | 沙箱 + AOT/JIT,瀏覽器與雲端皆有用 |
| .NET CoreCLR | C#/F# | tiered JIT、AOT 也支援 |
| mruby / mruby/c | Ruby (subset) | 為嵌入式設計、極小 footprint |
| MicroPython | Python | MCU 上的 Python |
小結#
- 語言 VM 是執行 bytecode 的執行環境,不是 system VM
- 用 VM 換取可攜、動態語意、沙箱、易實作、GC、JIT
- 主流形式:tree-walker(最慢)、bytecode VM(主流)、JIT(最快)
- 兩大流派:stack-based(編碼小)vs register-based(dispatch 少)
- 整體架構:lexer / parser / AST ➡️ emitter ➡️ bytecode ➡️ loader ➡️ dispatch ➡️ values + frames + heap + GC + object model
下一章把這個架構具體拆開,看每個元件做什麼。