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

  • 2019 年 11 月 13 日
  • 筆記

如果你是一名 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 方法,JVM 會在當前執行緒的 Java 方法棧中生成一個棧幀,用以存放局部變數以及位元組碼的操作數。棧幀的大小是提前計算好的,而且 JVM 不要求棧幀在記憶體空間里連續分布。

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

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

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

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

整個 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 採用基於計數器的熱點探測,計數器又分為方法調用計數器和回邊計數器。