1995 Java:虛擬機、位元組碼與「寫一次到處跑」#
案發現場#
1990 年代初,Sun Microsystems 的 James Gosling 團隊面對兩個謎題:
- 跨平台之謎:C/C++ 編譯出的機器碼綁死特定 CPU 與作業系統(見 1972-C語言.md、1983-Cplusplus.md)。同一段程式在 Solaris、Windows、Macintosh 上要各編譯一次、各調試一輪。嵌入式市場更糟——晶片種類繁多,不可能為每種晶片維護一套程式。
- 記憶體安全之謎: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\) 綁定」變成走兩段映射:
跨平台之謎就此破解:只需為每種機器實作一次 JVM,而所有程式共享它。
第二步:破解記憶體安全之謎。 Java 的設計取捨:
- 無指標:只有物件參考(reference),且不可做算術運算——消滅指標越界。
- 垃圾回收:繼承 LISP 的 GC 思想(見 1958-LISP.md),程式設計師不
free記憶體,GC 自動回收不可達物件。 - 強型別 + 陣列邊界檢查:執行期檢查
a[i]是否越界,消滅緩衝區溢位。 - 沙箱(sandbox)安全模型:來自網路的 Applet 在受限環境執行,不可讀寫本地檔案、不可隨意連網——這是「從網路下載一段程式就執行」能被信任的前提。
第三步:攻克效能質疑。 位元組碼逐條解譯很慢。Sun 的解法是 JIT(Just-In-Time)編譯:JVM 觀察哪些程式碼被頻繁執行(熱點),在執行期即時把它們編譯成機器碼:
這樣冷程式碼不需要多餘編譯成本,熱程式碼則獲得接近原生的速度——而且 JIT 能根據執行期實際資訊(哪個類別真的被呼叫)做靜態編譯器做不到的優化(如去虛擬化)。
結案報告#
1995 年 5 月 Java 正式發表,口號「Write Once, Run Anywhere」風靡業界。三�皆破:跨平台(JVM)、記憶體安全(GC + 無指標)、網路安全(沙箱)。
遺產清單:
- VM 成為主流架構:Java 的成功驗證了虛擬機路線。Microsoft 的 C#/.NET CLR(見 2000-Csharp與DotNet.md)、Python 的位元組碼 VM(見 1991-Python.md)皆是同一思想的變體;WebAssembly(見 2016-WebAssembly.md)可視為「瀏覽器版 JVM」。
- JIT 編譯學科:HotSpot 的熱點偵測、方法內聯、去虛擬化成為 JIT 編譯器標準技術,JavaScript 的 V8 引擎後來青出於藍(見 1995-JavaScript.md)。
- 企業伺服器霸權:J2EE(Servlet、EJB)成為 2000 年代企業後端標準;Android 選擇 Java 為主要語言,讓它獲得第二春。
- GC 語言普及化:Java 證明 GC 語言可用於大型工業系統,終結了「GC 太慢」的偏見。
- 語法遺產: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 架構的縮影:原始碼 → 位元組碼 → 任何機器上的虛擬機執行——把語言與機器解耦的那一刀,切出了此後三十年的軟體世界。