作為一名Java程序員,你應(yīng)該知道,Java代碼有很多種不同的運(yùn)行方式。比如說可以在開發(fā)工具中運(yùn)行,可以雙擊執(zhí)行jar文件運(yùn)行,也可以在命令行中運(yùn)行,甚至可以在網(wǎng)頁中運(yùn)行。當(dāng)然,這些執(zhí)行方式都離不開JRE,也就是Java運(yùn)行時(shí)環(huán)境。實(shí)際上,JRE僅包含運(yùn)行Java程序的必需組件,包括Java虛擬機(jī)以及Java核心類庫等。我們Java程序員經(jīng)常接觸到的JDK(Java開發(fā)工具包)同樣包含了JRE,并且還附帶了一系列開發(fā)、診斷工具。然而,運(yùn)行C++代碼則無需額外的運(yùn)行時(shí)。我們往往把這些代碼直接編譯成CPU所能理解的代碼格式,也就是機(jī)器碼。
既然C++的運(yùn)行方式如此成熟,那么你有沒有想過,為什么Java要在虛擬機(jī)中運(yùn)行呢,Java虛擬機(jī)具體又是怎樣運(yùn)行Java代碼的呢,它的運(yùn)行效率又如何呢?
為什么Java要在虛擬機(jī)里運(yùn)行?
Java作為一門高級(jí)程序語言,它的語法非常復(fù)雜,抽象程度也很高。因此,直接在硬件上運(yùn)行這種復(fù)雜的程序并不現(xiàn)實(shí)。所以呢,在運(yùn)行Java程序之前,我們需要對(duì)其進(jìn)行一番轉(zhuǎn)換。
這個(gè)轉(zhuǎn)換具體是怎么操作的呢?當(dāng)前的主流思路是這樣子的,設(shè)計(jì)一個(gè)面向Java語言特性的虛擬機(jī),并通過編譯器將Java程序轉(zhuǎn)換成該虛擬機(jī)所能識(shí)別的指令序列,也稱Java字節(jié)碼。這里順便說一句,之所以這么取名,是因?yàn)镴ava字節(jié)碼指令的操作碼(opcode)被固定為一個(gè)字節(jié)。
舉例來說,下圖的中間列,正是用Java寫的Helloworld程序編譯而成的字節(jié)碼??梢钥吹剑cC版本的編譯結(jié)果一樣,都是由一個(gè)個(gè)字節(jié)組成的。
并且,我們同樣可以將其反匯編為人類可讀的代碼格式(如下圖的最右列所示)。不同的是,Java版本的編譯結(jié)果相對(duì)精簡一些。這是因?yàn)镴ava虛擬機(jī)相對(duì)于物理機(jī)而言,抽象程度更高。
#最左列是偏移;中間列是給虛擬機(jī)讀的機(jī)器碼;最右列是給人讀的代碼
0x00;b20002;getstaticjava.lang.System.out
0x03;1203;ldc"Hello,World!"
0x05;b60004;invokevirtualjava.io.PrintStream.println
0x08;b1;return
Java虛擬機(jī)可以由硬件實(shí)現(xiàn),但更為常見的是在各個(gè)現(xiàn)有平臺(tái)(如Windows、Linux)上提供軟件實(shí)現(xiàn)。這么做的意義在于,一旦一個(gè)程序被轉(zhuǎn)換成Java字節(jié)碼,那么它便可以在不同平臺(tái)上的虛擬機(jī)實(shí)現(xiàn)里運(yùn)行。這也就是我們經(jīng)常說的“一次編寫,到處運(yùn)行”。
虛擬機(jī)的另外一個(gè)好處是它帶來了一個(gè)托管環(huán)境(ManagedRuntime)。這個(gè)托管環(huán)境能夠代替我們處理一些代碼中冗長而且容易出錯(cuò)的部分。其中最廣為人知的當(dāng)屬自動(dòng)內(nèi)存管理與垃圾回收,這部分內(nèi)容甚至催生了一波垃圾回收調(diào)優(yōu)的業(yè)務(wù)。
除此之外,托管環(huán)境還提供了諸如數(shù)組越界、動(dòng)態(tài)類型、安全權(quán)限等等的動(dòng)態(tài)檢測(cè),使我們免于書寫這些無關(guān)業(yè)務(wù)邏輯的代碼。
Java虛擬機(jī)具體是怎樣運(yùn)行Java字節(jié)碼的?
下面以標(biāo)準(zhǔn)JDK中的HotSpot虛擬機(jī)為例,從虛擬機(jī)以及底層硬件兩個(gè)角度,大概講一講Java虛擬機(jī)具體是怎么運(yùn)行Java字節(jié)碼的。
從虛擬機(jī)視角來看,執(zhí)行Java代碼首先需要將它編譯而成的class文件加載到Java虛擬機(jī)中。加載后的Java類會(huì)被存放于方法區(qū)(MethodArea)中。實(shí)際運(yùn)行時(shí),虛擬機(jī)會(huì)執(zhí)行方法區(qū)內(nèi)的代碼。
Java虛擬機(jī)在內(nèi)存中劃分出堆和棧來存儲(chǔ)運(yùn)行時(shí)數(shù)據(jù),虛擬機(jī)會(huì)將棧細(xì)分為面向Java方法的Java方法棧,面向本地方法(用C++寫的native方法)的本地方法棧,以及存放各個(gè)線程執(zhí)行位置的PC寄存器。
在運(yùn)行過程中,每當(dāng)調(diào)用進(jìn)入一個(gè)Java方法,Java虛擬機(jī)會(huì)在當(dāng)前線程的Java方法棧中生成一個(gè)棧幀,用以存放局部變量以及字節(jié)碼的操作數(shù)。這個(gè)棧幀的大小是提前計(jì)算好的,而且Java虛擬機(jī)不要求棧幀在內(nèi)存空間里連續(xù)分布。
當(dāng)退出當(dāng)前執(zhí)行的方法時(shí),不管是正常返回還是異常返回,Java虛擬機(jī)均會(huì)彈出當(dāng)前線程的當(dāng)前棧幀,并將之舍棄。
從硬件視角來看,Java字節(jié)碼無法直接執(zhí)行。因此,Java虛擬機(jī)需要將字節(jié)碼翻譯成機(jī)器碼。
在HotSpot里面,上述翻譯過程有兩種形式:第一種是解釋執(zhí)行,即逐條將字節(jié)碼翻譯成機(jī)器碼并執(zhí)行;第二種是即時(shí)編譯(Just-In-Timecompilation,JIT),即將一個(gè)方法中包含的所有字節(jié)碼編譯成機(jī)器碼后再執(zhí)行。
前者的優(yōu)勢(shì)在于無需等待編譯,而后者的優(yōu)勢(shì)在于實(shí)際運(yùn)行速度更快。HotSpot默認(rèn)采用混合模式,綜合了解釋執(zhí)行和即時(shí)編譯兩者的優(yōu)點(diǎn)。它會(huì)先解釋執(zhí)行字節(jié)碼,而后將其中反復(fù)執(zhí)行的熱點(diǎn)代碼,以方法為單位進(jìn)行即時(shí)編譯。
Java虛擬機(jī)的運(yùn)行效率究竟是怎樣的?
HotSpot采用了多種技術(shù)來提升啟動(dòng)性能以及峰值性能,剛剛提到的即時(shí)編譯便是其中最重要的技術(shù)之一。
即時(shí)編譯建立在程序符合二八定律的假設(shè)上,也就是百分之二十的代碼占據(jù)了百分之八十的計(jì)算資源。
對(duì)于占據(jù)大部分的不常用的代碼,我們無需耗費(fèi)時(shí)間將其編譯成機(jī)器碼,而是采取解釋執(zhí)行的方式運(yùn)行;另一方面,對(duì)于僅占據(jù)小部分的熱點(diǎn)代碼,我們則可以將其編譯成機(jī)器碼,以達(dá)到理想的運(yùn)行速度。
理論上講,即時(shí)編譯后的Java程序的執(zhí)行效率,是可能超過C++程序的。這是因?yàn)榕c靜態(tài)編譯相比,即時(shí)編譯擁有程序的運(yùn)行時(shí)信息,并且能夠根據(jù)這個(gè)信息做出相應(yīng)的優(yōu)化。
舉個(gè)例子,我們知道虛方法是用來實(shí)現(xiàn)面向?qū)ο笳Z言多態(tài)性的。對(duì)于一個(gè)虛方法調(diào)用,盡管它有很多個(gè)目標(biāo)方法,但在實(shí)際運(yùn)行過程中它可能只調(diào)用其中的一個(gè)。
這個(gè)信息便可以被即時(shí)編譯器所利用,來規(guī)避虛方法調(diào)用的開銷,從而達(dá)到比靜態(tài)編譯的C++程序更高的性能。
為了滿足不同用戶場(chǎng)景的需要,HotSpot內(nèi)置了多個(gè)即時(shí)編譯器:C1、C2和Graal。Graal是Java10正式引入的實(shí)驗(yàn)性即時(shí)編譯器,之所以引入多個(gè)即時(shí)編譯器,是為了在編譯時(shí)間和生成代碼的執(zhí)行效率之間進(jìn)行取舍。C1又叫做Client編譯器,面向的是對(duì)啟動(dòng)性能有要求的客戶端GUI程序,采用的優(yōu)化手段相對(duì)簡單,因此編譯時(shí)間較短。
C2又叫做Server編譯器,面向的是對(duì)峰值性能有要求的服務(wù)器端程序,采用的優(yōu)化手段相對(duì)復(fù)雜,因此編譯時(shí)間較長,但同時(shí)生成代碼的執(zhí)行效率較高。
從Java7開始,HotSpot默認(rèn)采用分層編譯的方式:熱點(diǎn)方法首先會(huì)被C1編譯,而后熱點(diǎn)方法中的熱點(diǎn)會(huì)進(jìn)一步被C2編譯。
為了不干擾應(yīng)用的正常運(yùn)行,HotSpot的即時(shí)編譯是放在額外的編譯線程中進(jìn)行的。HotSpot會(huì)根據(jù)CPU的數(shù)量設(shè)置編譯線程的數(shù)目,并且按1:2的比例配置給C1及C2編譯器。
在計(jì)算資源充足的情況下,字節(jié)碼的解釋執(zhí)行和即時(shí)編譯可同時(shí)進(jìn)行。編譯完成后的機(jī)器碼會(huì)在下次調(diào)用該方法時(shí)啟用,以替換原本的解釋執(zhí)行。
總結(jié)
Java代碼之所以要在虛擬機(jī)中運(yùn)行,是因?yàn)樗峁┝丝梢浦残?。一旦Java代碼被編譯為Java字節(jié)碼,便可以在不同平臺(tái)上的Java虛擬機(jī)實(shí)現(xiàn)上運(yùn)行。此外,虛擬機(jī)還提供了一個(gè)代碼托管的環(huán)境,代替我們處理部分冗長而且容易出錯(cuò)的事務(wù),例如內(nèi)存管理。
Java虛擬機(jī)將運(yùn)行時(shí)內(nèi)存區(qū)域劃分為五個(gè)部分,分別為方法區(qū)、堆、PC寄存器、Java方法棧和本地方法棧。Java程序編譯而成的class文件,需要先加載至方法區(qū)中,方能在Java虛擬機(jī)中運(yùn)行。
為了提高運(yùn)行效率,標(biāo)準(zhǔn)JDK中的HotSpot虛擬機(jī)采用的是一種混合執(zhí)行的策略。
它會(huì)解釋執(zhí)行Java字節(jié)碼,然后會(huì)將其中反復(fù)執(zhí)行的熱點(diǎn)代碼,以方法為單位進(jìn)行即時(shí)編譯,翻譯成機(jī)器碼后直接運(yùn)行在底層硬件之上。
HotSpot裝載了多個(gè)不同的即時(shí)編譯器,以便在編譯時(shí)間和生成代碼的執(zhí)行效率之間做取舍。
以上就是動(dòng)力Java培訓(xùn)機(jī)構(gòu)小編介紹的“JVM是如何運(yùn)行Java代碼的”的內(nèi)容,希望對(duì)大家有幫助,如有疑問,請(qǐng)?jiān)诰€咨詢,有專業(yè)老師隨時(shí)為你服務(wù)。