Java虛擬機器啟動過程解析

Java知識圖譜 發表於 2022-05-27
Java

一、序言

當我們在編寫Java應用的時候,很少會注意Java程式是如何被執行的,如何被作業系統管理和排程的。帶著好奇心,探索一下Java虛擬機器啟動過程。

1、素材準備

Java原始碼Java位元組碼Java虛擬機器作業系統四個角度分解啟動過程。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("HelloWorld!");
    }
}
2、原始碼生成位元組碼

利用Java環境提供的可執行命令javac將原始碼編譯成位元組碼檔案,編譯後的位元組碼檔案與平臺無關,可跨平臺執行。注意區分javac命令是一個獨立的編譯應用,原始碼編譯完成,程式終止。java命令啟動的虛擬機器程式的編譯過程是將位元組碼指令編譯成彙編指令(二進位制指令)。

3、虛擬機器解析位元組碼

Java位元組碼無法直接在作業系統上建立程式,因此需要藉助已經啟動的虛擬機器程式來解析位元組碼,處理位元組碼有兩種常見方式:解釋型編譯型

在命令列中每執行java命令代表啟動一個Java虛擬機器程式,各虛擬機器相互獨立,通過命令列引數分別對虛擬機器程式進行配置。

Java虛擬機器準備啟動完畢後,便可以依次解析位元組碼指令,正式執行Java程式碼部分。

4、作業系統管理虛擬機器

作業系統通過程式管理和排程Java虛擬機器,無法感知虛擬機器間接解析Java位元組碼部分。Java位元組碼通過虛擬機器的抽象,完成了在作業系統上執行。

二、Java虛擬機器

當執行Java應用時,需要先安裝Java環境,然而安裝的Java環境與Java應用有什麼關係,Java應用是如何執行起來的,下面一探究竟。

二進位制可執行程式${JAVA_HOME}/bin/java是C++編寫經過GCC編譯器編譯後形成的,探索Java虛擬機器的執行原理,首先需要找到相應的原始碼。

當在安裝Java環境時,會看到一個src.zip 壓縮檔案,解壓后里面launcher/java.c檔案便是可執行檔案java命令的主要原始碼。

虛擬機器的啟動入口位於launcher/java.cmain方法,整個流程分為如下幾個步驟: 配置JVM裝載環境;解析虛擬機器引數;設定執行緒棧大小;執行Java main方法

(一)配置JVM裝載環境

從作業系統載入環境變數、硬體資訊等執行環境資訊,為後續建立JVM程式做準備。

(二)命令列引數解析

裝載完JVM環境之後,需要對啟動時命令列引數進行解析,該過程通過ParseArguments方法實現,並呼叫AddOption方法將解析完成的引數儲存到JavaVMOption中。

比如常見的JavaVMOption引數在此步驟解析:

 -Xms:設定堆的初始值InitialHeapSize,也是堆的最小值; 
 -Xmx:設定堆的最大值MaxHeapSize;

JVM調優各引數解析便是在此步驟完成的。

(三)執行main方法

執行緒棧大小確定後,通過ContinueInNewThread方法建立新執行緒,並執行JavaMain函式,大概流程如下:

1、新建JVM例項

InitializeJVM方法呼叫InvocationFunctions的CreateJavaVM方法,即呼叫JVM.dll函式JNI_CreateJavaVM,新建一個JVM例項,該過程比較複雜。

2、載入入口類

通常在命令列中執行如下命令即指明入口類路徑

# 直接指名入口類路徑
java HelloWorld.class
# 通過包類配置入口類路徑
java -jar HelloWorld.jar
3、查詢main方法

通過GetStaticMethodID方法查詢指定main方法名的靜態方法。

4、執行main方法

通過JavaCalls::call回撥執行main方法。需要注意的是,這裡執行main方法不是Java語言的方法,是經過虛擬機器解釋(或者編譯)後,作業系統能夠理解的二進位制可執行方法。

三、解析位元組碼

(一)解釋位元組碼

1、基於棧指令集
iconst_1    將 1 放入棧頂
iconst_1    將 1 放入棧頂
iadd        將棧頂的 2 個數相加後結果放入棧頂
istore_0    將相加的結果放入區域性變數表

基於棧的指令集優點是虛擬機器直譯器是可跨平臺移植的,換句話說不同平臺的虛擬機器直譯器程式碼可以複用。

2、基於暫存器指令集
mov eax,1 把 EAX 暫存器的值設為 1
add eax,1 再把這個值加 1 ,結果儲存在了 EAX 暫存器

基於暫存器指令集的優點是執行速度相對於棧較快,原因是出棧入棧本身就涉及了大量的指令,而且棧是在記憶體中實現的,更底層的彙編指令效能更高。

基於暫存器指令集的缺點是虛擬機器直譯器是不可跨平臺移植,需要針對不同平臺的虛擬機器做不同實現。考慮到不同平臺已經使用不同的虛擬機器程式,因此此過程多使用者透明。


虛擬機器通過直譯器來翻譯位元組碼檔案中的指令比較順其自然,可是對於伺服器端高頻執行的程式來說,中間的翻譯過程相對耗時。解釋位元組碼的方式適用於對啟動效能要求高,並且執行頻率較低的應用程式。

(二)編譯位元組碼

最初,JVM 中的位元組碼是由直譯器( Interpreter )完成編譯的,當虛擬機器發現某個方法或程式碼塊的執行特別頻繁的時候,就會把這些程式碼認定為熱點程式碼

為了提高熱點程式碼的執行效率,在執行時,即時編譯器(JIT,Just In Time)會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各層次的優化,然後儲存到記憶體中。

在 HotSpot 虛擬機器中,內建了兩種 JIT,分別為C1 編譯器C2 編譯器,這兩個編譯器的編譯過程是不一樣的。

1、C1 編譯器

C1 編譯器是一個簡單快速的編譯器,主要的關注點在於區域性性的優化,適用於執行時間較短或對啟動效能有要求的程式,也稱為Client Compiler,例如,GUI 應用對介面啟動速度就有一定要求。

2、C2 編譯器

C2 編譯器是為長期執行的伺服器端應用程式做效能調優的編譯器,適用於執行時間較長或對峰值效能有要求的程式,也稱為Server Compiler,例如,伺服器上長期執行的 Java 應用對穩定執行就有一定的要求。

3、分層編譯

分層編譯將 JVM 的執行狀態分為了 5 個層次:

第 0 層:程式解釋執行,預設開啟效能監控功能(Profiling),如果不開啟,可觸發第二層編譯;
第 1 層:可稱為 C1 編譯,將位元組碼編譯為原生程式碼,進行簡單、可靠的優化,不開啟 Profiling;
第 2 層:也稱為 C1 編譯,開啟 Profiling,僅執行帶方法呼叫次數和迴圈回邊執行次數 profiling 的 C1 編譯;
第 3 層:也稱為 C1 編譯,執行所有帶 Profiling 的 C1 編譯;
第 4 層:可稱為 C2 編譯,也是將位元組碼編譯為原生程式碼,但是會啟用一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化。

通常情況下,C2 的執行效率比 C1 高出30%以上。

在 Java8 中,預設開啟分層編譯。如果只想開啟 C2,可以關閉分層編譯(-XX:-TieredCompilation),如果只想用 C1,可以在開啟分層編譯的同時,使用引數:-XX:TieredStopAtLevel=1

通過 java -version命令列可以檢視到當前虛擬機器解析位元組碼的方式,mixed mode表示既有解釋模式也有即是編譯模式。

java version "1.8.0_261"
Java(TM) SE Runtime Environment (build 1.8.0_261-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)

mixed mode代表是預設的混合編譯模式,除了這種模式外,我們還可以使用-Xint引數強制虛擬機器執行於只有直譯器的編譯模式下;也可以使用引數-Xcomp強制虛擬機器執行於只有 JIT 的編譯模式下。

僅使用解釋模式

通過命令java -Xint -version設定僅使用解釋模式,interpreted mode表示解釋模式。

java version "1.8.0_261"
Java(TM) SE Runtime Environment (build 1.8.0_261-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, interpreted mode)

僅使用編譯模式

通過命令java -Xcomp -version設定僅使用編譯模式,compiled mode表示編譯模式。在編譯模式下,程式啟動能感覺到明顯的卡頓。

java version "1.8.0_261"
Java(TM) SE Runtime Environment (build 1.8.0_261-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, compiled mode)

四、小結

通過對Java虛擬機器啟動過程的解析,特別是即時編譯環節的理解,Java應用執行並不慢。當應用中熱點程式碼普遍被編譯成彙編指令(二進位制可執行命令)存放於記憶體中時,可近似達到C語言原生程式的執行速度。

隨著算力與記憶體成本日漸降低,通過空間複雜度置換時間複雜度的策略顯然是合理的,使用Java語言編寫需求萬千變化的應用是第一選擇:既有跨平臺、記憶體安全、框架生態豐富的優點,也在執行效率方面積極改善,這種折中選擇與市場反饋保持一致。