如果你是一名 Java 開發人員,你肯定指定 Java 代碼有很多種不同的運行方式。比如說可以在開發工具(IDEA、Eclipse等)中運行,可以雙擊執行 jar 文件運行,也可以在命令行中運行,甚至可以在網頁(比如各種 OJ)中運行。當然,這些執行方式都離不開 JRE(Java 運行時環境)。 J ...
如果你是一名 Java 開發人員,你肯定指定 Java 代碼有很多種不同的運行方式。比如說可以在開發工具(IDEA、Eclipse等)中運行,可以雙擊執行 jar 文件運行,也可以在命令行中運行,甚至可以在網頁(比如各種 OJ)中運行。當然,這些執行方式都離不開 JRE(Java 運行時環境)。
JRE 包含運行 Java 程式的必需組件,包括 JVM(Java 虛擬機)以及 Java 核心類庫等。Java 程式員經常接觸到的 JDK(Java 開發工具包)同樣包含了 JRE,並且還附帶了一系列開發、診斷工具。
本篇文章主要針對以下兩個問題和大家一起探討:
- 為什麼需要 JVM?
- JVM 是怎樣運行 Java 代碼的呢?
為什麼需要 JVM?
Java 的一個非常重要的特點就是與平臺的無關性,而使用 JVM 是實現這一特點的關鍵。Java 作為一門高級程式語言,語法複雜,抽象程度高。因此,直接在硬體上運行這種複雜的程式並不現實。所以在運行 Java 程式之前,我們需要對其進行轉換。
設計一個面向 Java 語言特性的虛擬機,並通過編譯器將 Java 程式轉換成該虛擬機所能識別的指令序列(因為 Java 位元組碼指令的操作碼(opcode)被固定為一個位元組,故又稱 Java 位元組碼)。
JVM 一般是在各個現有平臺(如 Windows、Linux)上提供軟體實現,這樣可以使一旦一個程式被轉換成 Java 位元組碼,那麼便可以在不同平臺上的虛擬機實現里運行(一次編寫,到處運行)。
JVM 另外一個好處是帶有托管環境(Managed Runtime),托管環境能夠代替處理一些代碼中冗長而且容易出錯的部分,其中包括自動記憶體管理與垃圾回收(GC)。
另外,托管環境還提供了諸如數組越界、動態類型、安全許可權等等的動態檢測,使我們免於書寫這些無關業務邏輯的代碼。
JVM 是怎樣運行 Java 代碼的呢?
JVM 具體是怎麼運行 Java 位元組碼的呢?下麵我們一起來看一下:
從 JVM 來看,執行 Java 代碼首先需要將它編譯而成的 class 文件載入到 JVM 中。載入後的 Java 類會被存放於方法區(Method Area)中。實際運行時,JVM 會執行方法區內的代碼。
JVM 會在記憶體中劃分出堆和棧來存儲運行時數據,JVM 會將棧細分為面向 Java 方法的 Java 方法棧,面向本地方法(用 C++ 寫的 native 方法)的本地方法棧,以及存放各個線程執行位置的 PC 寄存器。
在運行過程中,每當調用進入一個 Java 方法,JVM 會在當前線程的 Java 方法棧中生成一個棧幀,用以存放局部變數以及位元組碼的操作數。棧幀的大小是提前計算好的,而且 JVM 不要求棧幀在記憶體空間里連續分佈。
當退出當前執行的方法時,不管是正常返回還是異常返回,JVM 均會彈出當前線程的當前棧幀,並將之捨棄。
從硬體視角來看,Java 位元組碼無法直接執行。因此,JVM 需要將位元組碼翻譯成機器碼。
在 HotSpot 裡面,上述翻譯過程有兩種形式:第一種是解釋執行(interpreter),即逐條將位元組碼翻譯成機器碼並執行;第二種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有位元組碼編譯成機器碼後再執行。
前者的優勢在於無需等待編譯,而後者的優勢在於實際運行速度更快。HotSpot 預設採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。它會先解釋執行位元組碼,而後將其中反覆執行的熱點代碼,以方法為單位進行即時編譯。
整個 Java 代碼執行過程如下:
- 使用 javac 把 .java 源文件編譯為位元組碼(文件尾碼名為 .class)
- 位元組碼經過 JIT 環境變數進行判斷,是否屬於熱點代碼(多次調用的方法或迴圈體)
- 熱點代碼使用 JIT 編譯為可執行的機器碼
- 非熱點代碼使用解釋器解釋執行所有位元組碼
其中,在運行過程中會被即時編譯的熱點代碼有兩類:
- 被多次調用的方法
- 被多次執行的迴圈體
針對第一類,編譯器會將整個方法作為編譯對象,這也是標準的 JIT 編譯方式。對於第二類是由迴圈體出發的,但是編譯器依然會以整個方法作為編譯對象,因為發生在方法執行過程中,稱為棧上替換。
HotSpot 採用了多種技術來提升啟動性能以及峰值性能,剛剛提到的即時編譯便是其中最重要的技術之一。
即時編譯建立在程式符合二八定律的假設上,也就是百分之二十的代碼占據了百分之八十的計算資源。
對於占據大部分的不常用的代碼,我們無需耗費時間將其編譯成機器碼,而是採取解釋執行的方式運行;另一方面,對於僅占據小部分的熱點代碼,我們則可以將其編譯成機器碼,以達到理想的運行速度。
為了滿足不同用戶場景的需要,HotSpot 內置了多個即時編譯器:C1、C2。之所以引入多個即時編譯器,是為了在編譯時間和生成代碼的執行效率之間進行取捨。
- C1 (Client 編譯器)面向的是對啟動性能有要求的客戶端 GUI 程式,採用的優化手段相對簡單,因此編譯時間較短。
- C2 (Server 編譯器)面向的是對峰值性能有要求的伺服器端程式,採用的優化手段相對複雜,因此編譯時間較長,但同時生成代碼的執行效率較高。
從 Java 7 開始,HotSpot 預設採用分層編譯的方式:熱點方法首先會被 C1 編譯,而後熱點方法中的熱點會進一步被 C2 編譯。
為了不幹擾應用的正常運行,HotSpot 的即時編譯是放在額外的編譯線程中進行的。HotSpot 會根據 CPU 的數量設置編譯線程的數目,並且按 1:2 的比例配置給 C1 及 C2 編譯器。
在計算資源充足的情況下,位元組碼的解釋執行和即時編譯可同時進行。編譯完成後的機器碼會在下次調用該方法時啟用,以替換原本的解釋執行。
其中判斷一段代碼是否為熱點代碼,是不是需要觸發即時編譯,這樣的行為稱為熱點探測(Hot Spot Detection),探測演算法有兩種:
- 基於採樣的熱點探測(Sample Based Hot Spot Detection):虛擬機會周期的對各個線程棧頂進行檢查,如果某些方法經常出現在棧頂,這個方法就是熱點方法。優點是實現簡單、高效,很容易獲取方法調用關係。缺點是很難確認方法的 reduce,容易受到線程阻塞或其他外因擾亂。
- 基於計數器的熱點探測(Counter Based Hot Spot Detection):為每個方法(甚至是代碼塊)建立計數器,執行次數超過閾值就認為是熱點方法。優點是統計結果精確嚴謹。缺點是實現麻煩,不能直接獲取方法的調用關係。
HotSpot 使用的是第二種-基於計數器的熱點探測,並且有兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)。
總結
這篇文章主要介紹了為什麼需要 JVM 以及 JVM 是怎樣運行 Java 代碼的。
為什麼需要 JVM:
- 提供了可移植性。一次編譯,到處執行。
- 提供了代碼托管的環境,代替處理部分冗長而且容易出錯的部分。
JVM 將運行時記憶體區域劃分為五個部分,分別為方法區、堆、PC 寄存器、Java 方法棧和本地方法棧。Java 程式編譯而成的 class 文件,需要先載入至方法區中,方能在 JVM 中運行。
為了提高運行效率,HotSpot 虛擬機採用的是一種混合執行的策略,會解釋執行 Java 位元組碼,然後會將其中反覆執行的熱點代碼,以方法為單位進行即時編譯,翻譯成機器碼後直接運行在底層硬體之上。
HotSpot 裝載了多個不同的即時編譯器,以便在編譯時間和生成代碼的執行效率之間做取捨。
判斷熱點代碼的探測演算法包括基於採樣和基於計數器兩種,HotSpot 採用基於計數器的熱點探測,計數器又分為方法調用計數器和回邊計數器。