跳轉到

1995 Java:虛擬機、位元組碼與「寫一次到處跑」#

案發現場#

1990 年代初,Sun Microsystems 的 James Gosling 團隊面對兩個謎題:

  1. 跨平台之謎:C/C++ 編譯出的機器碼綁死特定 CPU 與作業系統(見 1972-C語言.md、1983-Cplusplus.md)。同一段程式在 Solaris、Windows、Macintosh 上要各編譯一次、各調試一輪。嵌入式市場更糟——晶片種類繁多,不可能為每種晶片維護一套程式。
  2. 記憶體安全之謎:C++ 的手動記憶體管理(指標、free)導致懸垂指標、緩衝區溢位、記憶體洩漏——這些 bug 難以重現,是系統崩潰與安全漏洞的主要來源。

Sun 原本的目標是機上盒(set-top box)市場,計畫名為 Green。網路時代來臨後,謎題升級:能否有一種程式,透過網路發佈一次,就在任何機器上安全地執行?

偵查過程#

Gosling 的推理核心是一個優雅的架構決定:把「編譯」與「執行」切成兩半,中間用虛擬機連接。

第一步:設計中間層——位元組碼(bytecode)。 Java 原始碼不直接編譯成機器碼,而是編譯成平台無關的位元組碼:

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}
javac HelloWorld.java   # 編譯成 HelloWorld.class(位元組碼)
java HelloWorld         # 任何平台的 JVM 都能執行

位元組碼是一種棧式虛擬機指令集:

getstatic #7        // 把 System.out 推上堆疊
ldc    #13          // 把 "Hello, World" 推上堆疊
invokevirtual #19   // 呼叫 println,參數取自堆疊
return

任何機器只要安裝 JVM(Java Virtual Machine)就能執行同一份 .class 檔案。數學化地說,程式從「與機器 \(M\) 綁定」變成走兩段映射:

\[ \text{Source} \xrightarrow{\text{javac}} \text{Bytecode} \xrightarrow{\text{JVM}_M} \text{機器 } M \text{ 執行} \]

跨平台之謎就此破解:只需為每種機器實作一次 JVM,而所有程式共享它。

第二步:破解記憶體安全之謎。 Java 的設計取捨:

  1. 無指標:只有物件參考(reference),且不可做算術運算——消滅指標越界。
  2. 垃圾回收:繼承 LISP 的 GC 思想(見 1958-LISP.md),程式設計師不 free 記憶體,GC 自動回收不可達物件。
  3. 強型別 + 陣列邊界檢查:執行期檢查 a[i] 是否越界,消滅緩衝區溢位。
  4. 沙箱(sandbox)安全模型:來自網路的 Applet 在受限環境執行,不可讀寫本地檔案、不可隨意連網——這是「從網路下載一段程式就執行」能被信任的前提。

第三步:攻克效能質疑。 位元組碼逐條解譯很慢。Sun 的解法是 JIT(Just-In-Time)編譯:JVM 觀察哪些程式碼被頻繁執行(熱點),在執行期即時把它們編譯成機器碼:

\[ \text{Bytecode} \xrightarrow{\text{解譯(冷碼)}} \text{執行}, \quad \text{Bytecode} \xrightarrow{\text{JIT(熱碼)}} \text{機器碼} \xrightarrow{\text{執行}} \]

這樣冷程式碼不需要多餘編譯成本,熱程式碼則獲得接近原生的速度——而且 JIT 能根據執行期實際資訊(哪個類別真的被呼叫)做靜態編譯器做不到的優化(如去虛擬化)。

結案報告#

1995 年 5 月 Java 正式發表,口號「Write Once, Run Anywhere」風靡業界。三�皆破:跨平台(JVM)、記憶體安全(GC + 無指標)、網路安全(沙箱)。

遺產清單:

  1. VM 成為主流架構:Java 的成功驗證了虛擬機路線。Microsoft 的 C#/.NET CLR(見 2000-Csharp與DotNet.md)、Python 的位元組碼 VM(見 1991-Python.md)皆是同一思想的變體;WebAssembly(見 2016-WebAssembly.md)可視為「瀏覽器版 JVM」。
  2. JIT 編譯學科:HotSpot 的熱點偵測、方法內聯、去虛擬化成為 JIT 編譯器標準技術,JavaScript 的 V8 引擎後來青出於藍(見 1995-JavaScript.md)。
  3. 企業伺服器霸權:J2EE(Servlet、EJB)成為 2000 年代企業後端標準;Android 選擇 Java 為主要語言,讓它獲得第二春。
  4. GC 語言普及化:Java 證明 GC 語言可用於大型工業系統,終結了「GC 太慢」的偏見。
  5. 語法遺產:Java 的 class/介面/繼承語法影響 C#、Kotlin、Swift(見 2014-Swift.md)。

諷刺的是,Java 最初押注的 Applet 市場徹底失敗(被 JavaScript 取代),但企業伺服器市場大獲全勝——偵探押錯了犯罪現場,卻正好站在另一個更大案子的出口。

證據與工具#

用 Python 模擬一個棧式虛擬機,執行 Java 風格的位元組碼:

class JVM:
    """迷你棧式虛擬機:執行 Java 風格位元組碼"""
    def __init__(self):
        self.stack = []      # 運算堆疊
        self.vars = {}       # 局部變數表

    def run(self, code):
        for ins in code:
            op, *args = ins.split()
            if op == 'push':    self.stack.append(int(args[0]))
            elif op == 'load':  self.stack.append(self.vars[args[0]])
            elif op == 'store': self.vars[args[0]] = self.stack.pop()
            elif op == 'iadd':  b, a = self.stack.pop(), self.stack.pop(); self.stack.append(a + b)
            elif op == 'imul':  b, a = self.stack.pop(), self.stack.pop(); self.stack.append(a * b)
            elif op == 'print': print(self.stack.pop())
            elif op == 'return': break
        return self.stack

# 對應 Java: int a = 3; int b = 4; System.out.println(a * b + a + b);
bytecode = [
    'push 3', 'store a',
    'push 4', 'store b',
    'load a', 'load b', 'imul',   # a * b
    'load a', 'iadd',             # + a
    'load b', 'iadd',             # + b
    'print', 'return'
]
JVM().run(bytecode)   # => 19

# 展示跨平台思想:同一份 bytecode,任何實作 JVM 的「機器」都能執行
# 這正是 1995 年「Write Once, Run Anywhere」的核心

執行後 a * b + a + b = 3*4 + 3 + 4 = 19。這個約 15 行的迷你 VM 正是 Java 架構的縮影:原始碼 → 位元組碼 → 任何機器上的虛擬機執行——把語言與機器解耦的那一刀,切出了此後三十年的軟體世界。