Java虛擬機啟動過程探索
一、序言
當(dāng)我們在編寫Java應(yīng)用的時候,很少會注意Java程序是如何被運行的,如何被操作系統(tǒng)管理和調(diào)度的。帶著好奇心,探索一下Java虛擬機啟動過程。
1、素材準備
從Java源代碼、Java字節(jié)碼、Java虛擬機、操作系統(tǒng)四個角度分解啟動過程。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("HelloWorld!");
}
}2、源代碼生成字節(jié)碼
利用Java環(huán)境提供的可執(zhí)行命令javac將源代碼編譯成字節(jié)碼文件,編譯后的字節(jié)碼文件與平臺無關(guān),可跨平臺運行。注意區(qū)分javac命令是一個獨立的編譯應(yīng)用,源代碼編譯完成,進程終止。java命令啟動的虛擬機進程的編譯過程是將字節(jié)碼指令編譯成匯編指令(二進制指令)。
3、虛擬機解析字節(jié)碼
Java字節(jié)碼無法直接在操作系統(tǒng)上創(chuàng)建進程,因此需要借助已經(jīng)啟動的虛擬機進程來解析字節(jié)碼,處理字節(jié)碼有兩種常見方式:解釋型和編譯型。
在命令行中每運行java命令代表啟動一個Java虛擬機進程,各虛擬機相互獨立,通過命令行參數(shù)分別對虛擬機進程進行配置。
Java虛擬機準備啟動完畢后,便可以依次解析字節(jié)碼指令,正式運行Java代碼部分。
4、操作系統(tǒng)管理虛擬機
操作系統(tǒng)通過進程管理和調(diào)度Java虛擬機,無法感知虛擬機間接解析Java字節(jié)碼部分。Java字節(jié)碼通過虛擬機的抽象,完成了在操作系統(tǒng)上運行。
二、Java虛擬機
當(dāng)運行Java應(yīng)用時,需要先安裝Java環(huán)境,然而安裝的Java環(huán)境與Java應(yīng)用有什么關(guān)系,Java應(yīng)用是如何運行起來的,下面一探究竟。
二進制可執(zhí)行程序${JAVA_HOME}/bin/java是C++編寫經(jīng)過GCC編譯器編譯后形成的,探索Java虛擬機的運行原理,首先需要找到相應(yīng)的源碼。
當(dāng)在安裝Java環(huán)境時,會看到一個src.zip壓縮文件,解壓后里面launcher/java.c文件便是可執(zhí)行文件java命令的主要源碼。
虛擬機的啟動入口位于launcher/java.c的main方法,整個流程分為如下幾個步驟: 配置JVM裝載環(huán)境;解析虛擬機參數(shù);設(shè)置線程棧大??;執(zhí)行Java main方法
(一)配置JVM裝載環(huán)境
從操作系統(tǒng)加載環(huán)境變量、硬件信息等運行環(huán)境信息,為后續(xù)創(chuàng)建JVM進程做準備。
(二)命令行參數(shù)解析
裝載完JVM環(huán)境之后,需要對啟動時命令行參數(shù)進行解析,該過程通過ParseArguments方法實現(xiàn),并調(diào)用AddOption方法將解析完成的參數(shù)保存到JavaVMOption中。
比如常見的JavaVMOption參數(shù)在此步驟解析:
-Xms:設(shè)置堆的初始值InitialHeapSize,也是堆的最小值;
-Xmx:設(shè)置堆的最大值MaxHeapSize;
JVM調(diào)優(yōu)各參數(shù)解析便是在此步驟完成的。
(三)執(zhí)行main方法
線程棧大小確定后,通過ContinueInNewThread方法創(chuàng)建新線程,并執(zhí)行JavaMain函數(shù),大概流程如下:
1、新建JVM實例
InitializeJVM方法調(diào)用InvocationFunctions的CreateJavaVM方法,即調(diào)用JVM.dll函數(shù)JNI_CreateJavaVM,新建一個JVM實例,該過程比較復(fù)雜。
2、加載入口類
通常在命令行中運行如下命令即指明入口類路徑
# 直接指名入口類路徑 java HelloWorld.class # 通過包類配置入口類路徑 java -jar HelloWorld.jar
3、查找main方法
通過GetStaticMethodID方法查找指定main方法名的靜態(tài)方法。
4、執(zhí)行main方法
通過JavaCalls::call回調(diào)執(zhí)行main方法。需要注意的是,這里執(zhí)行main方法不是Java語言的方法,是經(jīng)過虛擬機解釋(或者編譯)后,操作系統(tǒng)能夠理解的二進制可執(zhí)行方法。
三、解析字節(jié)碼
(一)解釋字節(jié)碼
1、基于棧指令集
iconst_1 將 1 放入棧頂 iconst_1 將 1 放入棧頂 iadd 將棧頂?shù)?2 個數(shù)相加后結(jié)果放入棧頂 istore_0 將相加的結(jié)果放入局部變量表
基于棧的指令集優(yōu)點是虛擬機解釋器是可跨平臺移植的,換句話說不同平臺的虛擬機解釋器代碼可以復(fù)用。
2、基于寄存器指令集
mov eax,1 把 EAX 寄存器的值設(shè)為 1 add eax,1 再把這個值加 1 ,結(jié)果保存在了 EAX 寄存器
基于寄存器指令集的優(yōu)點是執(zhí)行速度相對于棧較快,原因是出棧入棧本身就涉及了大量的指令,而且棧是在內(nèi)存中實現(xiàn)的,更底層的匯編指令性能更高。
基于寄存器指令集的缺點是虛擬機解釋器是不可跨平臺移植,需要針對不同平臺的虛擬機做不同實現(xiàn)??紤]到不同平臺已經(jīng)使用不同的虛擬機程序,因此此過程多用戶透明。
虛擬機通過解釋器來翻譯字節(jié)碼文件中的指令比較順其自然,可是對于服務(wù)器端高頻執(zhí)行的程序來說,中間的翻譯過程相對耗時。解釋字節(jié)碼的方式適用于對啟動性能要求高,并且執(zhí)行頻率較低的應(yīng)用程序。
(二)編譯字節(jié)碼
最初,JVM 中的字節(jié)碼是由解釋器( Interpreter )完成編譯的,當(dāng)虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁的時候,就會把這些代碼認定為熱點代碼。
為了提高熱點代碼的執(zhí)行效率,在運行時,即時編譯器(JIT,Just In Time)會把這些代碼編譯成與本地平臺相關(guān)的機器碼,并進行各層次的優(yōu)化,然后保存到內(nèi)存中。
在 HotSpot 虛擬機中,內(nèi)置了兩種 JIT,分別為C1 編譯器和C2 編譯器,這兩個編譯器的編譯過程是不一樣的。
1、C1 編譯器
C1 編譯器是一個簡單快速的編譯器,主要的關(guān)注點在于局部性的優(yōu)化,適用于執(zhí)行時間較短或?qū)有阅苡幸蟮某绦?,也稱為Client Compiler,例如,GUI 應(yīng)用對界面啟動速度就有一定要求。
2、C2 編譯器
C2 編譯器是為長期運行的服務(wù)器端應(yīng)用程序做性能調(diào)優(yōu)的編譯器,適用于執(zhí)行時間較長或?qū)Ψ逯敌阅苡幸蟮某绦?,也稱為Server Compiler,例如,服務(wù)器上長期運行的 Java 應(yīng)用對穩(wěn)定運行就有一定的要求。
3、分層編譯
分層編譯將 JVM 的執(zhí)行狀態(tài)分為了 5 個層次:
第 0 層:程序解釋執(zhí)行,默認開啟性能監(jiān)控功能(Profiling),如果不開啟,可觸發(fā)第二層編譯;
第 1 層:可稱為 C1 編譯,將字節(jié)碼編譯為本地代碼,進行簡單、可靠的優(yōu)化,不開啟 Profiling;
第 2 層:也稱為 C1 編譯,開啟 Profiling,僅執(zhí)行帶方法調(diào)用次數(shù)和循環(huán)回邊執(zhí)行次數(shù) profiling 的 C1 編譯;
第 3 層:也稱為 C1 編譯,執(zhí)行所有帶 Profiling 的 C1 編譯;
第 4 層:可稱為 C2 編譯,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化。
通常情況下,C2 的執(zhí)行效率比 C1 高出30%以上。
在 Java8 中,默認開啟分層編譯。如果只想開啟 C2,可以關(guān)閉分層編譯(-XX:-TieredCompilation),如果只想用 C1,可以在打開分層編譯的同時,使用參數(shù):-XX:TieredStopAtLevel=1。
通過 java -version命令行可以查看到當(dāng)前虛擬機解析字節(jié)碼的方式,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參數(shù)強制虛擬機運行于只有解釋器的編譯模式下;也可以使用參數(shù)-Xcomp強制虛擬機運行于只有 JIT 的編譯模式下。
僅使用解釋模式
通過命令java -Xint -version設(shè)置僅使用解釋模式,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設(shè)置僅使用編譯模式,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)
四、小結(jié)
通過對Java虛擬機啟動過程的解析,特別是即時編譯環(huán)節(jié)的理解,Java應(yīng)用運行并不慢。當(dāng)應(yīng)用中熱點代碼普遍被編譯成匯編指令(二進制可執(zhí)行命令)存放于內(nèi)存中時,可近似達到C語言原生程序的運行速度。
隨著算力與內(nèi)存成本日漸降低,通過空間復(fù)雜度置換時間復(fù)雜度的策略顯然是合理的,使用Java語言編寫需求萬千變化的應(yīng)用是第一選擇:既有跨平臺、內(nèi)存安全、框架生態(tài)豐富的優(yōu)點,也在運行效率方面積極改善,這種折中選擇與市場反饋保持一致。
到此這篇關(guān)于Java虛擬機啟動過程解析的文章就介紹到這了,更多相關(guān)Java虛擬機啟動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea 無法創(chuàng)建Scala class 選項的原因分析及解決辦法匯總
這篇文章主要介紹了idea 無法創(chuàng)建Scala class 選項的解決辦法匯總,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
解決logback-classic 使用testCompile的打包問題
這篇文章主要介紹了解決logback-classic 使用testCompile的打包問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

