1. 程式人生 > >JVM是如何執行Java程式碼的

JVM是如何執行Java程式碼的

背景知識

 

作為一名 Java 程式設計師,你應該知道,Java 程式碼有很多種不同的執行方式。比如說可以在開發工具中執行,可以雙擊執行 jar 檔案執行,也可以在命令列中執行,甚至可以在網頁中執行。當然,這些執行方式都離不開 JRE,也就是 Java 執行時環境。實際上,JRE 僅包含執行 Java 程式的必需元件,包括 Java 虛擬機器以及 Java 核心類庫等。我們 Java 程式設計師經常接觸到的 JDK(Java 開發工具包)同樣包含了 JRE,並且還附帶了一系列開發、診斷工具。然而,執行 C++ 程式碼則無需額外的執行時。我們往往把這些程式碼直接編譯成 CPU 所能理解的程式碼格式,也就是機器碼。

既然 C++ 的執行方式如此成熟,那麼你有沒有想過,為什麼 Java 要在虛擬機器中執行呢,Java 虛擬機器具體又是怎樣執行 Java 程式碼的呢,它的執行效率又如何呢?

為什麼Java要在虛擬機器裡執行?

Java 作為一門高階程式語言,它的語法非常複雜,抽象程度也很高。因此,直接在硬體上執行這種複雜的程式並不現實。所以呢,在執行 Java 程式之前,我們需要對其進行一番轉換。

這個轉換具體是怎麼操作的呢?當前的主流思路是這樣子的,設計一個面向 Java 語言特性的虛擬機器,並通過編譯器將 Java 程式轉換成該虛擬機器所能識別的指令序列,也稱 Java 位元組碼。這裡順便說一句,之所以這麼取名,是因為 Java 位元組碼指令的操作碼(opcode)被固定為一個位元組。

舉例來說,下圖的中間列,正是用 Java 寫的 Helloworld 程式編譯而成的位元組碼。可以看到,它與 C 版本的編譯結果一樣,都是由一個個位元組組成的。

並且,我們同樣可以將其反彙編為人類可讀的程式碼格式(如下圖的最右列所示)。不同的是,Java 版本的編譯結果相對精簡一些。這是因為 Java 虛擬機器相對於物理機而言,抽象程度更高。

# 最左列是偏移;中間列是給虛擬機器讀的機器碼;最右列是給人讀的程式碼

0x00; b2 00 02; getstatic java.lang.System.out

0x03; 12 03; ldc "Hello, World!"

0x05; b6 00 04; invokevirtual java.io.PrintStream.println

0x08; b1; return

 

Java 虛擬機器可以由硬體實現,但更為常見的是在各個現有平臺(如 Windows、Linux)上提供軟體實現。這麼做的意義在於,一旦一個程式被轉換成 Java 位元組碼,那麼它便可以在不同平臺上的虛擬機器實現裡執行。這也就是我們經常說的“一次編寫,到處執行”。

虛擬機器的另外一個好處是它帶來了一個託管環境(Managed Runtime)。這個託管環境能夠代替我們處理一些程式碼中冗長而且容易出錯的部分。其中最廣為人知的當屬自動記憶體管理與垃圾回收,這部分內容甚至催生了一波垃圾回收調優的業務。

除此之外,託管環境還提供了諸如陣列越界、動態型別、安全許可權等等的動態檢測,使我們免於書寫這些無關業務邏輯的程式碼。

Java虛擬機器具體是怎樣執行Java位元組碼的?

下面以標準 JDK 中的 HotSpot 虛擬機器為例,從虛擬機器以及底層硬體兩個角度,大概講一講 Java 虛擬機器具體是怎麼執行 Java 位元組碼的。

從虛擬機器視角來看,執行 Java 程式碼首先需要將它編譯而成的 class 檔案載入到 Java 虛擬機器中。載入後的 Java 類會被存放於方法區(Method Area)中。實際執行時,虛擬機器會執行方法區內的程式碼。

Java 虛擬機器在記憶體中劃分出堆和棧來儲存執行時資料,虛擬機器會將棧細分為面向 Java 方法的 Java 方法棧,面向本地方法(用 C++ 寫的 native 方法)的本地方法棧,以及存放各個執行緒執行位置的 PC 暫存器。

如果有正在學java的程式設計師,可來我們的java技術學習扣qun哦:72340,3928,小編花了近一個月整理了一份非常適合18年學習的java乾貨,加入就免費送java的視訊教程噢!而且我每天晚上都會在裡面直播講Java知識,從零基礎學習到有基礎進階,歡迎初學和進階中的小夥伴。

在執行過程中,每當呼叫進入一個 Java 方法,Java 虛擬機器會在當前執行緒的 Java 方法棧中生成一個棧幀,用以存放區域性變數以及位元組碼的運算元。這個棧幀的大小是提前計算好的,而且 Java 虛擬機器不要求棧幀在記憶體空間裡連續分佈。

當退出當前執行的方法時,不管是正常返回還是異常返回,Java 虛擬機器均會彈出當前執行緒的當前棧幀,並將之捨棄。

從硬體視角來看,Java 位元組碼無法直接執行。因此,Java 虛擬機器需要將位元組碼翻譯成機器碼。

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

 

 

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

Java虛擬機器的執行效率究竟是怎樣的?

HotSpot 採用了多種技術來提升啟動效能以及峰值效能,剛剛提到的即時編譯便是其中最重要的技術之一。

即時編譯建立在程式符合二八定律的假設上,也就是百分之二十的程式碼佔據了百分之八十的計算資源。

對於佔據大部分的不常用的程式碼,我們無需耗費時間將其編譯成機器碼,而是採取解釋執行的方式執行;另一方面,對於僅佔據小部分的熱點程式碼,我們則可以將其編譯成機器碼,以達到理想的執行速度。

理論上講,即時編譯後的 Java 程式的執行效率,是可能超過 C++ 程式的。這是因為與靜態編譯相比,即時編譯擁有程式的執行時資訊,並且能夠根據這個資訊做出相應的優化。

舉個例子,我們知道虛方法是用來實現面嚮物件語言多型性的。對於一個虛方法呼叫,儘管它有很多個目標方法,但在實際執行過程中它可能只調用其中的一個。

這個資訊便可以被即時編譯器所利用,來規避虛方法呼叫的開銷,從而達到比靜態編譯的 C++ 程式更高的效能。

為了滿足不同使用者場景的需要,HotSpot 內建了多個即時編譯器:C1、C2 和 Graal。Graal 是 Java 10 正式引入的實驗性即時編譯器,之所以引入多個即時編譯器,是為了在編譯時間和生成程式碼的執行效率之間進行取捨。C1 又叫做 Client 編譯器,面向的是對啟動效能有要求的客戶端 GUI 程式,採用的優化手段相對簡單,因此編譯時間較短。

C2 又叫做 Server 編譯器,面向的是對峰值效能有要求的伺服器端程式,採用的優化手段相對複雜,因此編譯時間較長,但同時生成程式碼的執行效率較高。

從 Java 7 開始,HotSpot 預設採用分層編譯的方式:熱點方法首先會被 C1 編譯,而後熱點方法中的熱點會進一步被 C2 編譯。

為了不干擾應用的正常執行,HotSpot 的即時編譯是放在額外的編譯執行緒中進行的。HotSpot 會根據 CPU 的數量設定編譯執行緒的數目,並且按 1:2 的比例配置給 C1 及 C2 編譯器。

在計算資源充足的情況下,位元組碼的解釋執行和即時編譯可同時進行。編譯完成後的機器碼會在下次呼叫該方法時啟用,以替換原本的解釋執行。

總結

  • Java程式碼之所以要在虛擬機器中執行,是因為它提供了可移植性。一旦 Java 程式碼被編譯為 Java 位元組碼,便可以在不同平臺上的 Java 虛擬機器實現上執行。此外,虛擬機器還提供了一個程式碼託管的環境,代替我們處理部分冗長而且容易出錯的事務,例如記憶體管理。
  • Java 虛擬機器將執行時記憶體區域劃分為五個部分,分別為方法區、堆、PC 暫存器、Java 方法棧和本地方法棧。Java 程式編譯而成的 class 檔案,需要先載入至方法區中,方能在 Java 虛擬機器中執行。
  • 為了提高執行效率,標準 JDK 中的 HotSpot 虛擬機器採用的是一種混合執行的策略。
  • 它會解釋執行 Java 位元組碼,然後會將其中反覆執行的熱點程式碼,以方法為單位進行即時編譯,翻譯成機器碼後直接執行在底層硬體之上。
  • HotSpot 裝載了多個不同的即時編譯器,以便在編譯時間和生成程式碼的執行效率之間做取捨。