提到 Java,人們首先想到 Java 程式語言,但事實上 Java 是一種技術,由四面向組成:Java 程式語言、Java 類別檔案格式、Java 虛擬機 (JVM) 與 Java 應用程式介面 (Java API)。關係如圖 1:

圖 1:Java 四個面向關係
執行期環境代表 Java 平台。開發人員編寫 Java 程式碼,編譯成位元碼,載入記憶體後由解釋器解釋執行,或由即時編譯器選擇性轉換成機器碼執行。Java 平台由 JVM 與 Java API 搭建,Java 語言是進入平台的通道。平台結構如圖 2:

JVM 處於核心位置,是程式與底層作業系統、硬體無關的關鍵。下方為移植介面(適配器與 Java 作業系統),JVM 透過移植介面在具體平台實現。上方為 Java 基礎類別庫、擴展類別庫及 API。利用 Java API 編寫的應用程式與 Applet 可在任何 Java 平台執行,因 JVM 實現程式與作業系統分離,達到平台無關性。
什麼是 JVM?通常談論 JVM 可能指:
本文主要針對第三種情況。可視為想像中的機器,在實體電腦上透過軟體模擬實現,擁有想像硬體(處理器、堆疊、暫存器)與指令系統。
JVM 生命週期有明確任務:執行 Java 程式。程式啟動產生 JVM 實例,程式結束實例消失。下面從 JVM 體系結構與執行過程兩方面深入研究。
JVM 可由不同廠商實現,廠商不同導致實現差異,但仍能實現跨平台特性,歸功於體系結構設計。
JVM 實例行為涉及子系統、儲存區域、資料型別與指令,描述抽象內部體系結構,目的不僅規定實現時內部結構,更重要的是提供嚴格定義外部行為的方式。每個 JVM 有兩種機制:
每個 JVM 包含五個運行時資料區:方法區、堆、Java 堆疊、程式計數器、本地方法堆疊。這五區與兩機制組成體系結構圖 3:

圖 3:JVM 體系結構
每個 JVM 實例擁有自己的方法區與堆,所有執行緒共享。載入類別檔案時,解析二進位資料中的類別資訊放入方法區;程式執行時,所有物件實例置於堆上;每個執行緒建立時擁有自己的程式計數器與 Java 堆疊,程式計數器指向下一條將執行指令,Java 堆疊儲存該執行緒呼叫 Java 方法狀態;本地方法呼叫狀態儲存於本地方法堆疊,依賴具體實現。
處於 JVM 核心位置,行為由指令集決定。雖規範詳細說明執行位元碼遇到指令時實現應做什麼,但對如何做鮮少著墨。JVM 支援約 248 個位元碼,每個執行基本 CPU 運算(如整數加到暫存器、子程式轉移等)。Java 指令集相當於 Java 程式組合語言。
指令包含單一位元組操作碼(指定執行操作)與 0 或多個操作數(提供參數/資料)。許多指令無操作數,僅由單一位元組操作碼構成。虛擬機內層迴圈執行過程:
do {
取一個操作碼位元組;
根據操作碼值執行一個動作;
} while (程式未結束)
指令系統簡單性使虛擬機執行過程簡單,有利提高執行效率。操作數數量與大小由操作碼決定。操作數超過一位元組時,儲存順序為高位位元組優先(大端序)。指令流一般位元組對齊,tableswitch 與 lookupswitch 例外,內部要求強制 4 位元組邊界對齊。
實現 JVM 不強制要求本地方法介面支援,甚至可完全沒有。Sun 實現 JNI 為可移植性考慮,也可設計其他本地介面取代 JNI,但設計實現較複雜,需確保垃圾回收器不會釋放正被本地方法呼叫的物件。
執行時資料區,類別實例(物件)從中分配空間,管理由垃圾回收負責:不給程式設計師顯式釋放物件能力。Java 不規定具體垃圾回收演算法,可依系統需求使用各種演算法。
類似傳統語言編譯後程式碼或 Unix 程序正文段。保存方法程式碼(編譯後 Java 程式碼)與符號表。目前實現中方法程式碼不包含在垃圾回收堆中,但計劃未來版本實現。每個類別檔案包含一個 Java 類別或介面的編譯後程式碼。類別檔案是 Java 語言執行程式碼檔案。為保證平台無關性,JVM 規範對類別檔案格式詳細說明,細節請參考 Sun JVM 規範。
保存機器執行狀態,類似微處理器專用暫存器。JVM 有四種暫存器:
pc:Java 程式計數器optop:指向操作數堆疊頂端指標frame:指向當前執行方法執行環境指標vars:指向當前執行方法區域變數區第一個變數指標體系結構圖中所述為第一種(程式計數器)。每個執行緒建立即擁有自己的程式計數器。執行 Java 方法時,包含該執行緒正在執行指令位址;執行本地方法時,程式計數器值未定義。
三個區域:區域變數區、執行環境區、操作數區。
每個 Java 方法使用固定大小區域變數集。依 vars 暫存器字偏移量定址。區域變數皆 32 位元。長整數與雙精度浮點數佔兩個區域變數空間,但按第一個區域變數索引定址。規範不要求 64 位元值 64 位元對齊。提供將區域變數值載入操作數堆疊指令,與將操作數堆疊值寫入區域變數指令。
包含動態連結、正常方法返回、例外捕捉資訊。
Error 或 Exception,為 Throwable 子類別。程式中原因:動態連結錯(找不到所需 class 檔案)、執行時錯(空指標引用)、使用 throw 陳述。發生例外時 JVM 採取:檢查當前方法關聯的 catch 子句表(含有效指令範圍、可處理例外型別、處理程式碼區塊位址)。匹配 catch 子句條件:導致例外指令在指令範圍內、發生例外型別為其可處理型別子型別。找到匹配則轉移至指定例外處理區塊執行;未找到則重複尋找直到所有巢狀 catch 子句檢查完。因從第一個匹配 catch 子句繼續執行,catch 子句表順序重要。Java 程式碼結構化,故總可將方法所有例外處理器按序排列表中,對任意程式計數器值,可用線性順序找到合適處理區塊。找不到匹配則目前方法得到「未捕獲例外」結果返回呼叫者,如同例外剛在呼叫者發生。若呼叫者仍無處理區塊,錯誤傳播下去。傳播到最頂層則系統呼叫預設例外處理區塊。機器指令只從操作數堆疊取操作數,操作後結果返回堆疊。選擇堆疊結構原因:在少量暫存器或非通用暫存器機器(如 Intel 486)上也能高效模擬虛擬機行為。操作數堆疊 32 位元。用於傳遞方法參數、接收結果、支援操作參數、保存操作結果。例:iadd 將兩整數相加。相加兩整數應為操作數堆疊頂兩字。由先前指令壓入堆疊。兩整數彈出、相加、結果壓回操作數堆疊。每個基本資料型別有專門指令操作。每個操作數在堆疊需一個儲存位置,long 與 double 需兩個。操作數只能被適用其型別操作碼操作。例:壓入兩 int,視為一 long 非法。Sun 實現中此限制由位元碼驗證器強制執行。少數操作(dup、swap)操作執行時資料區不考慮型別。
執行緒呼叫本地方法時,不再受虛擬機結構與安全限制約束,可存取虛擬機執行期資料區,也可使用本地處理器與任何型別堆疊。例:本地堆疊為 C 語言堆疊,C 程式呼叫 C 函數時,參數以某順序壓入堆疊,結果返回呼叫函數。實現 JVM 時本地方法介面用 C 模型堆疊,則本地方法堆疊調度與使用完全與 C 語言堆疊相同。
透過具體範例分析執行過程。JVM 透過呼叫指定類別 main 方法啟動,傳遞字串陣列參數,使指定類別被載入,同時連結該類別使用的其他型別並初始化。例:
class HelloApp {
public static void main(String[] args) {
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
}
編譯後命令列輸入:java HelloApp run virtual machine
透過呼叫 HelloApp.main 啟動 JVM,傳遞包含三字串 run、virtual、machine 陣列。略述 JVM 執行 HelloApp 可能步驟:
HelloApp.main,發現類別未載入(JVM 不包含該類別二進位代表),使用 ClassLoader 尋找。失敗則拋出例外。main 呼叫前,必須對 HelloApp 與其他型別連結再初始化。連結三階段:驗證、準備、解析。驗證檢查載入主類別符號與語意;準備建立類別/介面靜態欄位並初始化為標準預設值;解析檢查主類別對其他類別/介面符號引用(可選)。整個過程如圖 4:

圖 4:虛擬機執行過程
本文透過對 JVM 體系結構深入研究及 Java 程式執行時 JVM 執行過程詳細分析,旨在剖析清楚 Java 虛擬機機制。