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語言編寫需求萬千變化的應用是第一選擇:既有跨平台、記憶體安全、框架生態豐富的優點,也在運行效率方面積極改善,這種折中選擇與市場回饋保持一致。