你編寫的Java代碼是咋跑起來的?

如果你是一名 Java 開發人員,你肯定指定 Java 代碼有很多種不同的運行方式。比如說可以在開發工具(IDEA、Eclipse等)中運行,可以雙擊執行 jar 文件運行,也可以在命令行中運行,甚至可以在網頁(比如各種 OJ)中運行。當然,這些執行方式都離不開 JRE(Java 運行時環境)。

JRE 包含運行 Java 程序的必需組件,包括 JVM(Java 虛擬機)以及 Java 核心類庫等。Java 程序員經常接觸到的 JDK(Java 開發工具包)同樣包含了 JRE,並且還附帶了一系列開發、診斷工具。

本篇文章主要針對以下兩個問題和大家一起探討:

  1. 為什麼需要 JVM?
  2. 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代碼是咋跑起來的?

在運行過程中,每當調用進入一個 Java 方法,JVM 會在當前線程的 Java 方法棧中生成一個棧幀,用以存放局部變量以及字節碼的操作數。棧幀的大小是提前計算好的,而且 JVM 不要求棧幀在內存空間裡連續分佈。

當退出當前執行的方法時,不管是正常返回還是異常返回,JVM 均會彈出當前線程的當前棧幀,並將之捨棄。

從硬件視角來看,Java 字節碼無法直接執行。因此,JVM 需要將字節碼翻譯成機器碼。

在 HotSpot 裡面,上述翻譯過程有兩種形式:第一種是解釋執行(interpreter),即逐條將字節碼翻譯成機器碼並執行;第二種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有字節碼編譯成機器碼後再執行。

你編寫的Java代碼是咋跑起來的?

前者的優勢在於無需等待編譯,而後者的優勢在於實際運行速度更快。HotSpot 默認採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而後將其中反覆執行的熱點代碼,以方法為單位進行即時編譯。

你編寫的Java代碼是咋跑起來的?

整個 Java 代碼執行過程如下:

  1. 使用 javac 把 .java 源文件編譯為字節碼(文件後綴名為 .class)
  2. 字節碼經過 JIT 環境變量進行判斷,是否屬於熱點代碼(多次調用的方法或循環體)
  3. 熱點代碼使用 JIT 編譯為可執行的機器碼
  4. 非熱點代碼使用解釋器解釋執行所有字節碼

其中,在運行過程中會被即時編譯的熱點代碼有兩類:

  1. 被多次調用的方法
  2. 被多次執行的循環體

針對第一類,編譯器會將整個方法作為編譯對象,這也是標準的 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),探測算法有兩種:

  1. 基於採樣的熱點探測(Sample Based Hot Spot Detection):虛擬機會週期的對各個線程棧頂進行檢查,如果某些方法經常出現在棧頂,這個方法就是熱點方法。優點是實現簡單、高效,很容易獲取方法調用關係。缺點是很難確認方法的 reduce,容易受到線程阻塞或其他外因擾亂。
  2. 基於計數器的熱點探測(Counter Based Hot Spot Detection):為每個方法(甚至是代碼塊)建立計數器,執行次數超過閾值就認為是熱點方法。優點是統計結果精確嚴謹。缺點是實現麻煩,不能直接獲取方法的調用關係。

HotSpot 使用的是第二種-基於計數器的熱點探測,並且有兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)。

總結

這篇文章主要介紹了為什麼需要 JVM 以及 JVM 是怎樣運行 Java 代碼的。

為什麼需要 JVM:

  1. 提供了可移植性。一次編譯,到處執行。
  2. 提供了代碼託管的環境,代替處理部分冗長而且容易出錯的部分。

JVM 將運行時內存區域劃分為五個部分,分別為方法區、堆、PC 寄存器、Java 方法棧和本地方法棧。Java 程序編譯而成的 class 文件,需要先加載至方法區中,方能在 JVM 中運行。

為了提高運行效率,HotSpot 虛擬機採用的是一種混合執行的策略,會解釋執行 Java 字節碼,然後會將其中反覆執行的熱點代碼,以方法為單位進行即時編譯,翻譯成機器碼後直接運行在底層硬件之上。

HotSpot 裝載了多個不同的即時編譯器,以便在編譯時間和生成代碼的執行效率之間做取捨。

判斷熱點代碼的探測算法包括基於採樣和基於計數器兩種,HotSpot 採用基於計數器的熱點探測,計數器又分為方法調用計數器和回邊計數器。


分享到:


相關文章: