「VM」這個詞的兩種用法#

容易混淆 ── 中文都翻「虛擬機器」,英文都是 VM,但根本不是同一回事

類別例子模擬什麼
System VMKVM、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    ;; 存到 a

VM 內部維護 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 lookupC++ 靜態 dispatch動態語言 / Java virtual call
常量摺疊多數做-
InliningAOT 編譯器JIT
死碼消除AOT 與 JIT-

「動態語言」其實就是「把更多工作推到執行期」。這給程式設計師彈性,給 VM 工程師頭痛 ── 而 JIT 就是把執行期觀察到的資訊回流到編譯,把優化重新拿回來。

一些值得知道的 VM#

VM語言特色
JVM HotSpotJava/Kotlintiered JIT、世代式 GC、極度成熟
CPythonPython簡單 stack VM、ref counting + GC
V8JavaScriptIgnition (bytecode) + TurboFan (JIT)
JavaScriptCoreJavaScriptLLInt / Baseline / DFG / FTL 四階層
Lua / LuaJITLua極簡 register VM;LuaJIT 是 tracing JIT
BEAMErlang/Elixir為大量併發 + 容錯設計
Wasm runtimeWasm沙箱 + AOT/JIT,瀏覽器與雲端皆有用
.NET CoreCLRC#/F#tiered JIT、AOT 也支援
mruby / mruby/cRuby (subset)為嵌入式設計、極小 footprint
MicroPythonPythonMCU 上的 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

下一章把這個架構具體拆開,看每個元件做什麼。