JVM執(zhí)行引擎的項(xiàng)目實(shí)踐
我們知道Java源文件經(jīng)過編譯之后生成class文件,class文件加載到內(nèi)存中,此時(shí)物理機(jī)器并不能直接執(zhí)行代碼,因?yàn)樗鼪]辦法識(shí)別class文件中的內(nèi)容,此時(shí)就需要執(zhí)行引擎(Execution Engine)來做相應(yīng)的處理。本貼將主要講解執(zhí)行引擎在JVM中的作用。
1、概述
執(zhí)行引擎是JVM核心的組成部分之一??梢园袹VM架構(gòu)分成三部分,如下圖所示:
執(zhí)行引擎位于JVM的最下層(圖中虛線框部分),可以粗略地看到執(zhí)行引擎負(fù)責(zé)和運(yùn)行時(shí)數(shù)據(jù)區(qū)交互。
2、計(jì)算機(jī)語言的發(fā)展史
在講解執(zhí)行引擎之前,需要知道什么是機(jī)器碼、匯編語言、高級(jí)語言以及為什么會(huì)有Java字節(jié)碼的出現(xiàn)。
2.1、機(jī)器碼
各種用0和1組成的二進(jìn)制編碼方式表示的指令,叫作機(jī)器指令碼,簡(jiǎn)稱機(jī)器碼。計(jì)算機(jī)發(fā)展的初始階段,人們就用機(jī)器碼編寫程序,我們也稱為機(jī)器語言。機(jī)器語言雖然能夠被計(jì)算機(jī)理解和接受,但和人類的語言差別太大,不易被人們理解和記憶,并且用它編程容易出差錯(cuò)。使用機(jī)器碼編寫的程序一經(jīng)輸入計(jì)算機(jī),CPU可以直接讀取運(yùn)行,因此和其他語言編的程序相比,執(zhí)行速度最快。機(jī)器碼與CPU緊密相關(guān),所以不同種類的CPU所對(duì)應(yīng)的機(jī)器碼也就不同。
2.2、匯編語言
由于機(jī)器碼是由0和1組成的二進(jìn)制序列,可讀性實(shí)在太差,于是人們發(fā)明了指令。指令就是把機(jī)器碼中特定的0和1序列,簡(jiǎn)化成對(duì)應(yīng)的指令(一般為英文簡(jiǎn)寫,如mov、inc等),可讀性稍好,這就是我們常說的匯編語言。在匯編語言中,用助記符(Mnemonics)代替機(jī)器碼的操作碼,用地址符號(hào)(Symbol)或標(biāo)號(hào)(Label)代替指令或操作數(shù)的地址。
不同的硬件平臺(tái),各自支持的指令是有差別的。因此每個(gè)平臺(tái)所支持的指令,稱為對(duì)應(yīng)平臺(tái)的指令集,如常見的x86指令集對(duì)應(yīng)的是x86架構(gòu)的平臺(tái),ARM指令集對(duì)應(yīng)的是ARM架構(gòu)的平臺(tái)。不同平臺(tái)之間指令不可以直接移植。
由于計(jì)算機(jī)只認(rèn)識(shí)機(jī)器碼,所以用匯編語言編寫的程序還必須翻譯成機(jī)器碼,計(jì)算機(jī)才能識(shí)別和執(zhí)行。
2.3、高級(jí)語言
為了使計(jì)算機(jī)用戶編程序更容易些,后來就出現(xiàn)了各種高級(jí)計(jì)算機(jī)語言。比如C、C++等更容易讓人識(shí)別的語言。
當(dāng)計(jì)算機(jī)執(zhí)行高級(jí)語言編寫的程序時(shí),仍然需要把程序解釋或編譯成機(jī)器的指令碼。完成這個(gè)過程的程序就叫作解釋程序或編譯程序,如下圖所示:
2.4、字節(jié)碼
字節(jié)碼是一種中間狀態(tài)(中間碼)的二進(jìn)制代碼(文件),需要轉(zhuǎn)譯后才能成為機(jī)器碼。字節(jié)碼主要為了實(shí)現(xiàn)特定軟件運(yùn)行和軟件環(huán)境,與硬件環(huán)境無關(guān)。如下圖所示:
Java程序可以通過編譯器將源碼編譯成Java字節(jié)碼,特定平臺(tái)上的虛擬機(jī)將字節(jié)碼轉(zhuǎn)譯為可以直接執(zhí)行的指令,也就實(shí)現(xiàn)了跨平臺(tái)性。
3、Java代碼編譯和執(zhí)行過程
我們知道虛擬機(jī)并不是真實(shí)存在的,是由軟件編寫而成的,它是相對(duì)于物理機(jī)的概念。但是虛擬機(jī)和物理機(jī)一樣都可以執(zhí)行一系列的計(jì)算機(jī)指令,其區(qū)別是物理機(jī)的執(zhí)行引擎是直接建立在處理器、緩存、指令集和操作系統(tǒng)層面上的,而虛擬機(jī)的執(zhí)行引擎則是由軟件自行實(shí)現(xiàn)的,因此可以不受物理?xiàng)l件制約地定制指令集與執(zhí)行引擎的結(jié)構(gòu)體系,執(zhí)行那些不被硬件直接支持的指令集格式。
JVM裝載字節(jié)碼到內(nèi)存中,但字節(jié)碼僅僅只是一個(gè)實(shí)現(xiàn)跨平臺(tái)的通用契約而已,它并不能夠直接運(yùn)行在操作系統(tǒng)之上,因?yàn)樽止?jié)碼指令并不等價(jià)于機(jī)器碼,它內(nèi)部包含的僅僅只是一些能夠被JVM所識(shí)別的字節(jié)碼指令、符號(hào)表,以及其他輔助信息。
如果想要讓一個(gè)Java程序運(yùn)行起來,執(zhí)行引擎的任務(wù)就是將字節(jié)碼指令解釋/編譯為對(duì)應(yīng)平臺(tái)上的機(jī)器碼才可以。簡(jiǎn)單來說,JVM中的執(zhí)行引擎充當(dāng)了將高級(jí)語言翻譯為機(jī)器語言的譯者,就好比兩個(gè)國家領(lǐng)導(dǎo)人之間的交流需要翻譯官一樣。
在Java虛擬機(jī)規(guī)范中制定了JVM執(zhí)行引擎的概念模型,這個(gè)概念模型成為各大發(fā)行商的JVM執(zhí)行引擎的統(tǒng)一規(guī)范。執(zhí)行引擎的工作流程如下圖所示:
- (1)執(zhí)行引擎在執(zhí)行的過程中需要執(zhí)行什么樣的字節(jié)碼指令完全依賴PC寄存器。
- (2)每當(dāng)執(zhí)行完一項(xiàng)指令操作后,PC寄存器就會(huì)更新下一條需要被執(zhí)行的指令地址。
- (3)方法在執(zhí)行的過程中,執(zhí)行引擎有可能會(huì)通過存儲(chǔ)在局部變量表中的對(duì)象引用準(zhǔn)確定位到存儲(chǔ)在Java堆區(qū)中的對(duì)象實(shí)例信息,以及通過對(duì)象頭中的元數(shù)據(jù)指針定位到目標(biāo)對(duì)象的類型信息。
所有的JVM的執(zhí)行引擎輸入、輸出都是一致的,輸入的是字節(jié)碼二進(jìn)制流,處理過程是字節(jié)碼解析執(zhí)行的過程,輸出的是執(zhí)行結(jié)果。
大部分的程序代碼轉(zhuǎn)換成物理機(jī)的目標(biāo)代碼或虛擬機(jī)能執(zhí)行的指令集之前,都需要經(jīng)過下圖中的各個(gè)步驟:
程序源碼到抽象語法樹的過程屬于代碼編譯的過程,和虛擬機(jī)無關(guān),如上個(gè)中矩形實(shí)線框所示;指令流到解釋執(zhí)行的過程屬于生成虛擬機(jī)指令集的過程,如上圖矩形虛線框所示;優(yōu)化器到目標(biāo)代碼的過程屬于生成物理機(jī)目標(biāo)代碼的過程,如上圖中最下層云狀圖形所示。
具體來說,Java代碼編譯是由Java源碼編譯器來完成,流程圖如下圖所示:
在Java中,javac編譯器主要負(fù)責(zé)詞法分析、語法分析和語義分析,最終生成二進(jìn)制字節(jié)碼,此過程發(fā)生在虛擬機(jī)外部。
Java源代碼經(jīng)過javac編譯器編譯之后生成字節(jié)碼,Java字節(jié)碼的執(zhí)行是由JVM執(zhí)行引擎來完成,流程圖如下圖所示:
可以看到圖中有JIT編譯器和字節(jié)碼解釋器兩種路徑執(zhí)行字節(jié)碼,也就是說可以解釋執(zhí)行,也可以編譯執(zhí)行。Java是一種解釋類型的語言,其實(shí)JDK 1.0時(shí)代,將Java語言定位為“解釋執(zhí)行”還是比較準(zhǔn)確的,再后來,Java也發(fā)展出可以直接生成本地代碼的編譯器,所以Java語言就不再是純粹的解釋執(zhí)行語言了?,F(xiàn)在JVM在執(zhí)行Java代碼的時(shí)候,通常都會(huì)將解釋執(zhí)行與編譯執(zhí)行結(jié)合起來進(jìn)行,這也就是為什么現(xiàn)在Java語言被稱為半編譯半解釋型語言的原因。
4、解釋器
解釋器的作用是當(dāng)JVM啟動(dòng)時(shí)會(huì)根據(jù)預(yù)定義的規(guī)范對(duì)字節(jié)碼采用逐行解釋的方式執(zhí)行,將每條字節(jié)碼文件中的內(nèi)容“翻譯”為對(duì)應(yīng)平臺(tái)的機(jī)器碼執(zhí)行。
JVM設(shè)計(jì)者的初衷是為了滿足Java程序?qū)崿F(xiàn)跨平臺(tái)特性,因此避免采用靜態(tài)編譯的方式直接生成機(jī)器碼,從而誕生了實(shí)現(xiàn)解釋器在運(yùn)行時(shí)采用逐行解釋字節(jié)碼執(zhí)行程序的想法。如下圖所示:
如果不采用字節(jié)碼文件的形式,我們就需要針對(duì)不同的平臺(tái)(Windows、Linux、Mac)編譯不同的機(jī)器指令,那么就需要耗費(fèi)很多精力和時(shí)間;如果采用了字節(jié)碼的形式,那么就只需要從源文件編譯到字節(jié)碼文件即可,雖然在不同的平臺(tái)上,但是JVM中的解釋器可以識(shí)別同一套字節(jié)碼文件,大大提高了開發(fā)效率。解釋器真正意義上所承擔(dān)的角色就是一個(gè)運(yùn)行時(shí)的“翻譯者”,將字節(jié)碼文件中的內(nèi)容“翻譯”為對(duì)應(yīng)平臺(tái)的機(jī)器碼執(zhí)行。
在Java的發(fā)展歷史里,一共有兩套解釋執(zhí)行器,分別是古老的字節(jié)碼解釋器和現(xiàn)在普遍使用的模板解釋器。字節(jié)碼解釋器在執(zhí)行時(shí)通過純軟件代碼模擬字節(jié)碼的執(zhí)行,效率非常低下。而模板解釋器將每一條字節(jié)碼和一個(gè)模板函數(shù)相關(guān)聯(lián),模板函數(shù)中直接產(chǎn)生這條字節(jié)碼執(zhí)行時(shí)的機(jī)器碼,從而很大程度上提高了解釋器的性能。在HotSpot VM中,解釋器主要由Interpreter模塊和Code模塊構(gòu)成。Interpreter模塊實(shí)現(xiàn)了解釋器的核心功能,Code模塊用于管理HotSpot VM在運(yùn)行時(shí)生成的機(jī)器碼。
由于解釋器在設(shè)計(jì)和實(shí)現(xiàn)上非常簡(jiǎn)單,因此除了Java語言之外,還有許多高級(jí)語言同樣也是基于解釋器執(zhí)行的,比如Python、Perl、Ruby等。但是在今天,基于解釋器執(zhí)行已經(jīng)淪落為低效的代名詞。為了解決低效這個(gè)問題,JVM平臺(tái)支持一種叫作即時(shí)編譯的技術(shù)。即時(shí)編譯的目的是避免函數(shù)被解釋執(zhí)行,而是將整個(gè)函數(shù)體編譯成機(jī)器碼,每次函數(shù)執(zhí)行時(shí),只執(zhí)行編譯后的機(jī)器碼即可,這種方式可以使執(zhí)行效率大幅度提升。不過無論如何,基于解釋器的執(zhí)行模式仍然為中間語言的發(fā)展做出了不可磨滅的貢獻(xiàn)。
5、JIT編譯器
JIT編譯器(Just In Time Compiler)的作用就是虛擬機(jī)將字節(jié)碼直接編譯成機(jī)器碼。但是現(xiàn)代虛擬機(jī)為了提高執(zhí)行效率,會(huì)使用即時(shí)編譯技術(shù)將方法編譯成機(jī)器碼后再執(zhí)行。
在JDK 1.0時(shí)代,JVM完全是解釋執(zhí)行的,隨著技術(shù)的發(fā)展,現(xiàn)在主流的虛擬機(jī)中大都包含了即時(shí)編譯器。
HotSpot VM是目前市面上高性能虛擬機(jī)的代表作之一。它采用解釋器與即時(shí)編譯器并存的架構(gòu)。在JVM運(yùn)行時(shí),解釋器和即時(shí)編譯器能夠相互協(xié)作,各自取長補(bǔ)短,盡力去選擇最合適的方式來權(quán)衡編譯本地代碼的時(shí)間和直接解釋執(zhí)行代碼的時(shí)間。
在此大家需要注意,無論是采用解釋器進(jìn)行解釋執(zhí)行,還是采用即時(shí)編譯器進(jìn)行編譯執(zhí)行,都是希望程序執(zhí)行要快。最終字節(jié)碼都需要被轉(zhuǎn)換為對(duì)應(yīng)平臺(tái)的機(jī)器碼。
可以使用jconsole工具查看程序的運(yùn)行情況,代碼如下所示:
結(jié)果如下圖所示:
可以看見用到了JIT編譯器。關(guān)于jconsole工具的使用可以查看后面的內(nèi)容。
5.1、為什么HotSpot VM同時(shí)存在JIT編譯器和解釋器
既然HotSpot VM中已經(jīng)內(nèi)置JIT編譯器了,那么為什么還需要再使用解釋器來“拖累”程序的執(zhí)行性能呢?比如JRockit VM內(nèi)部就不包含解釋器,字節(jié)碼全部都依靠即時(shí)編譯器編譯后執(zhí)行。
首先明確,當(dāng)程序啟動(dòng)后,解釋器可以馬上發(fā)揮作用,省去編譯的時(shí)間,立即執(zhí)行。編譯器要想發(fā)揮作用,把代碼編譯成機(jī)器碼,需要一定的執(zhí)行時(shí)間,但編譯為機(jī)器碼后,執(zhí)行效率高。
盡管JRockit VM中程序的執(zhí)行性能會(huì)非常高效,但程序在啟動(dòng)時(shí)必然需要花費(fèi)更長的時(shí)間來進(jìn)行編譯(即“預(yù)熱”)。對(duì)于服務(wù)端應(yīng)用來說,啟動(dòng)時(shí)間并非是關(guān)注重點(diǎn),但對(duì)于那些看中啟動(dòng)時(shí)間的應(yīng)用場(chǎng)景而言,或許就需要采用解釋器與即時(shí)編譯器并存的架構(gòu)來換取一個(gè)平衡點(diǎn)。在此模式下,當(dāng)JVM啟動(dòng)時(shí),解釋器可以首先發(fā)揮作用,而不必等待JIT全部編譯完成后再執(zhí)行,這樣可以省去許多不必要的編譯時(shí)間。隨著程序運(yùn)行時(shí)間的推移,即時(shí)編譯器逐漸發(fā)揮作用,根據(jù)熱點(diǎn)代碼探測(cè)功能,將有價(jià)值的字節(jié)碼編譯為機(jī)器碼,并緩存起來,以換取更高的程序執(zhí)行效率。
注意解釋執(zhí)行與編譯執(zhí)行在線上環(huán)境存在微妙的辯證關(guān)系。機(jī)器在熱機(jī)狀態(tài)可以承受的負(fù)載要大于冷機(jī)狀態(tài)。如果以熱機(jī)狀態(tài)時(shí)的流量進(jìn)行切流,可能使處于冷機(jī)狀態(tài)的服務(wù)器因無法承載流量而假死。
在生產(chǎn)環(huán)境發(fā)布過程中,以分批的方式進(jìn)行發(fā)布,根據(jù)機(jī)器數(shù)量劃分成多個(gè)批次,每個(gè)批次的機(jī)器數(shù)至多占到整個(gè)集群的1/8。曾經(jīng)有這樣的故障案例,某程序員在發(fā)布平臺(tái)進(jìn)行分批發(fā)布,在輸入發(fā)布總批數(shù)時(shí),誤填寫成分為兩批發(fā)布。如果是熱機(jī)狀態(tài),在正常情況下一半的機(jī)器可以勉強(qiáng)承載流量,但由于剛啟動(dòng)的JVM均是解釋執(zhí)行,還沒有進(jìn)行熱點(diǎn)代碼統(tǒng)計(jì)和JIT動(dòng)態(tài)編譯,導(dǎo)致機(jī)器啟動(dòng)之后,當(dāng)前l(fā)/2發(fā)布成功的服務(wù)器馬上全部宕機(jī),此故障證明了JIT的存在。
如下圖所示:
以人類語言為例,形象生動(dòng)地展示了Java語言中前端編譯器、解釋器和后端編譯器(即JIT編譯器)共同工作的流程。前端編譯器將不同的語言統(tǒng)一編譯成字節(jié)碼文件(即“烏拉庫哈嗎喲”),這些信息我們是看不懂的,而是供JVM來讀取的。之后可以通過解釋器逐行將字節(jié)碼指令解釋為本地機(jī)器指令執(zhí)行,或者通過JIT把熱點(diǎn)代碼編譯為本地機(jī)器指令執(zhí)行。
在此我們要說明一點(diǎn),Java語言的“編譯期”其實(shí)是一段“不確定”的操作過程,因?yàn)樗赡苁侵敢粋€(gè)前端編譯器(其實(shí)叫“編譯器的前端”更準(zhǔn)確一些)把.java文件轉(zhuǎn)變成.class文件的過程。也可能是指虛擬機(jī)的后端運(yùn)行期編譯器(JIT編譯器,Just In Time Compiler)把字節(jié)碼轉(zhuǎn)變成機(jī)器碼的過程。還可能是指使用靜態(tài)提前編譯器(AOT編譯器,Ahead Of Time Compiler)直接把.java文件編譯成本地機(jī)器代碼的過程。
5.2、熱點(diǎn)代碼探測(cè)確定何時(shí)JIT
是否需要啟動(dòng)JIT編譯器將字節(jié)碼直接編譯為對(duì)應(yīng)平臺(tái)的機(jī)器碼需要根據(jù)代碼被調(diào)用執(zhí)行的頻率而定。那些需要被編譯為本地代碼的字節(jié)碼也被稱為“熱點(diǎn)代碼”,JIT編譯器在運(yùn)行時(shí)會(huì)針對(duì)那些頻繁被調(diào)用的“熱點(diǎn)代碼”做出深度優(yōu)化,將其直接編譯為對(duì)應(yīng)平臺(tái)的機(jī)器碼,以此提升Java程序的執(zhí)行性能。
一個(gè)被多次調(diào)用的方法,或者是一個(gè)方法體內(nèi)部循環(huán)次數(shù)較多的循環(huán)體都可以被稱為“熱點(diǎn)代碼”,因此都可以通過JIT編譯器編譯為機(jī)器碼,并緩存起來。
一個(gè)方法被多次調(diào)用的時(shí)候,從解釋執(zhí)行切換到編譯執(zhí)行是在兩次方法調(diào)用之間產(chǎn)生的,因?yàn)樯弦淮畏椒ㄔ诒徽{(diào)用的時(shí)候還沒有將該方法編譯好,所以仍然需要繼續(xù)解釋執(zhí)行,而不需要去等待程序被編譯,否則太浪費(fèi)時(shí)間了,等再次調(diào)用該方法的時(shí)候,發(fā)現(xiàn)該方法已經(jīng)被編譯好,那么就會(huì)使用編譯好的機(jī)器碼執(zhí)行了。
還有一種情況就是一個(gè)方法體內(nèi)包含大量的循環(huán)的代碼,比如下面的代碼:
main()方法被執(zhí)行的次數(shù)只有一次,但是方法體內(nèi)部有一個(gè)循環(huán)20000次的循環(huán)體,這種情況下,就需要將循環(huán)體編譯為機(jī)器碼,而不是將main()方法編譯為機(jī)器碼,這個(gè)時(shí)候就需要在循環(huán)入口處判斷是否該循環(huán)體已經(jīng)被編譯為機(jī)器碼。由于這種編譯方式不需要等待方法的執(zhí)行結(jié)束,因此也被稱為棧上替換編譯,或簡(jiǎn)稱OSR(On Stack Replacement)編譯。
一個(gè)方法究竟要被調(diào)用多少次,或者一個(gè)循環(huán)體究竟需要執(zhí)行多少次循環(huán)才可以達(dá)到這個(gè)標(biāo)準(zhǔn)?必然需要一個(gè)明確的閾值,JIT編譯器才會(huì)將這些“熱點(diǎn)代碼”編譯為機(jī)器碼執(zhí)行。這里主要依靠熱點(diǎn)探測(cè)功能,比如上面代碼的循環(huán)次數(shù)為20000次,那么就可能在循環(huán)執(zhí)行5000次的時(shí)候開始被編譯,然后在第5200次循環(huán)的時(shí)候開始使用機(jī)器碼,中間的20次循環(huán)依然是解釋執(zhí)行,因?yàn)榫幾g也是需要消耗時(shí)間的。
目前HotSpot VM所采用的熱點(diǎn)探測(cè)方式是基于計(jì)數(shù)器的熱點(diǎn)探測(cè)。HotSpot VM會(huì)為每一個(gè)方法都建立兩個(gè)不同類型的計(jì)數(shù)器,分別為方法調(diào)用計(jì)數(shù)器(Invocation Counter)和回邊計(jì)數(shù)器(Back Edge Counter)。方法調(diào)用計(jì)數(shù)器用于統(tǒng)計(jì)方法的調(diào)用次數(shù),回邊計(jì)數(shù)器則用于統(tǒng)計(jì)循環(huán)體執(zhí)行的循環(huán)次數(shù)。
方法調(diào)用計(jì)數(shù)器的默認(rèn)閾值在Client模式下是1500次,在Server模式下是10000次。超過這個(gè)閾值,就會(huì)觸發(fā)JIT編譯。這個(gè)閾值可以通過虛擬機(jī)參數(shù)-XX:CompileThreshold來手動(dòng)設(shè)定。
一般而言,如果以缺省參數(shù)啟動(dòng)Java程序,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的是一段時(shí)間之內(nèi)方法被調(diào)用的次數(shù)。當(dāng)超過一定的時(shí)間限度,如果方法的調(diào)用次數(shù)沒有達(dá)到方法調(diào)用計(jì)數(shù)器的閾值,這個(gè)方法的調(diào)用計(jì)數(shù)器的數(shù)值調(diào)整為當(dāng)前數(shù)值的1/2,比如10分鐘之內(nèi)方法調(diào)用計(jì)數(shù)器數(shù)值為1000,下次執(zhí)行該方法的時(shí)候,方法調(diào)用計(jì)數(shù)器的數(shù)值從500開始計(jì)數(shù)。這個(gè)過程稱為方法調(diào)用計(jì)數(shù)器熱度的衰減(Counter Decay),而這段時(shí)間就稱為此方法統(tǒng)計(jì)的半衰周期(Counter Half Life Time),可以使用-XX:CounterHalfLifeTime參數(shù)設(shè)置半衰周期的時(shí)間,單位是秒??梢允褂肑VM參數(shù)“-XX:-UseCounterDecay”關(guān)閉熱度衰減,讓方法計(jì)數(shù)器統(tǒng)計(jì)方法調(diào)用的絕對(duì)次數(shù),這樣,只要系統(tǒng)運(yùn)行時(shí)間足夠長,絕大部分方法都會(huì)被編譯成機(jī)器碼。一般而言,如果項(xiàng)目規(guī)模不大,并且產(chǎn)品上線后很長一段時(shí)間不需要進(jìn)行版本迭代,都可以嘗試把熱度衰減關(guān)閉,這樣可以使Java程序在線上運(yùn)行的時(shí)間越久,執(zhí)行性能會(huì)更佳。
如圖下圖所示:
當(dāng)一個(gè)方法被調(diào)用時(shí),會(huì)先檢查該方法是否存在被JIT編譯過的版本,如果存在,則編譯執(zhí)行。如果不存在已被編譯過的版本,則將此方法的調(diào)用計(jì)數(shù)器值加1,然后判斷方法計(jì)數(shù)器的數(shù)值是否超過設(shè)置的閾值。如果已超過閾值,那么將會(huì)向JIT申請(qǐng)代碼編譯,如果沒有超過閾值,則繼續(xù)解釋執(zhí)行。
回邊計(jì)數(shù)器的作用是統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”(Back Edge),回邊可簡(jiǎn)單理解為循環(huán)末尾跳轉(zhuǎn)到循環(huán)開始?;剡呌?jì)數(shù)器的流程如下所示,當(dāng)程序執(zhí)行過程中遇到回邊指令時(shí),判斷是否已經(jīng)存在編譯的機(jī)器碼,如果存在,則編譯執(zhí)行即可,如果不存在,則回邊計(jì)數(shù)器加1,再次判斷是否超過閾值,如果沒有超過,則解釋執(zhí)行,如果超過閾值,則向編譯器提交編譯請(qǐng)求,之后編譯器開始編譯代碼,程序繼續(xù)解釋執(zhí)行?;剡呌?jì)數(shù)器的閾值可以通過參數(shù)“-XX:OnStackReplacePercentage”設(shè)置。顯然,建立回邊計(jì)數(shù)器統(tǒng)計(jì)的目的就是為了觸發(fā)OSR編譯,如下圖所示:
5.3、設(shè)置執(zhí)行模式
缺省情況下HotSpot VM采用解釋器與即時(shí)編譯器并存的架構(gòu),使用java –version命令可以查看,如下所示,mixed mode表示解釋器與即時(shí)編譯器并存。
當(dāng)然,開發(fā)人員可以根據(jù)具體的應(yīng)用場(chǎng)景,通過下面的命令顯式地為JVM指定在運(yùn)行時(shí)到底是完全采用解釋器執(zhí)行,還是完全采用即時(shí)編譯器執(zhí)行。
-Xint命令表示完全采用解釋器模式執(zhí)行程序,如下所示:
C:\Users\Administrator>java -Xint -version java version"1.8.0_131" Java(TM)SE Runtime Environment(build 1.8.0_131-b11) Java HotSpot(TM)64-Bit Server VM(build 25.131-b11,interpreted mode)
-Xcomp命令表示完全采用即時(shí)編譯器模式執(zhí)行程序。如果即時(shí)編譯出現(xiàn)問題,解釋器會(huì)介入執(zhí)行,如下所示:
C:\Users\Administrator>java -Xcomp -version java version"1.8.0_131" Java(TM)SE Runtime Environment(build 1.8.0_131-b11) Java HotSpot(TM)64-Bit Server VM(build 25.131-b11,compiled mode)
-Xmixed命令表示采用解釋器和即時(shí)編譯器的混合模式共同執(zhí)行程序,如下所示:
C:\Users\Administrator>java -Xmixed -version java version"1.8.0_131" Java(TM)SE Runtime Environment(build 1.8.0_131-b11) Java HotSpot(TM)64-Bit Server VM(build 25.131-b11,mixed mode)
5.4、C1編譯器和C2編譯器
在HotSpot VM中內(nèi)嵌有兩個(gè)JIT編譯器,分別為Client Compiler和Server Compiler,通常簡(jiǎn)稱為C1編譯器和C2編譯器。開發(fā)人員可以通過如下命令顯式指定JVM在運(yùn)行時(shí)到底使用哪一種即時(shí)編譯器。
- (1)-client:指定JVM運(yùn)行在Client模式下,并使用C1編譯器。C1編譯器會(huì)對(duì)字節(jié)碼進(jìn)行簡(jiǎn)單和可靠的優(yōu)化,耗時(shí)短,以達(dá)到更快的編譯速度。
- (2)-server:指定JVM運(yùn)行在Server模式下,并使用C2編譯器。C2進(jìn)行耗時(shí)較長的優(yōu)化,以及激進(jìn)優(yōu)化,但優(yōu)化的代碼執(zhí)行效率更高。
在不同的編譯器上有不同的優(yōu)化策略,C1編譯器上主要有方法內(nèi)聯(lián),去虛擬化、冗余消除。
- (1)方法內(nèi)聯(lián):將引用的函數(shù)代碼編譯到引用點(diǎn)處,這樣可以減少棧幀的生成,減少參數(shù)傳遞以及跳轉(zhuǎn)過程。
- (2)去虛擬化:對(duì)唯一的實(shí)現(xiàn)類進(jìn)行內(nèi)聯(lián)。
- (3)冗余消除:在運(yùn)行期間把一些不會(huì)執(zhí)行的代碼折疊掉。
C2的優(yōu)化主要是在全局層面,逃逸分析是優(yōu)化的基礎(chǔ)。基于逃逸分析在C2上有如下幾種優(yōu)化。
- (1)標(biāo)量替換:用標(biāo)量值代替聚合對(duì)象的屬性值。
- (2)棧上分配:對(duì)于未逃逸的對(duì)象分配對(duì)象在棧而不是堆。
- (3)同步消除:清除同步操作,通常指synchronized。
Java分層編譯(Tiered Compilation)策略:不開啟性能監(jiān)控的情況下,程序解釋執(zhí)行可以觸發(fā)C1編譯,將字節(jié)碼編譯成機(jī)器碼,可以進(jìn)行簡(jiǎn)單優(yōu)化。如果開啟性能監(jiān)控,C2編譯會(huì)根據(jù)性能監(jiān)控信息進(jìn)行激進(jìn)優(yōu)化。不過在Java 7版本之后,一旦開發(fā)人員在程序中顯式指定命令“-server”時(shí),默認(rèn)將會(huì)開啟分層編譯策略,由C1編譯器和C2編譯器相互協(xié)作共同來執(zhí)行編譯任務(wù)。一般來講,JIT編譯出來的機(jī)器碼性能比解釋器高。C2編譯器啟動(dòng)時(shí)長比C1編譯器慢,系統(tǒng)穩(wěn)定執(zhí)行以后,C2編譯器執(zhí)行速度遠(yuǎn)遠(yuǎn)快于C1編譯器。
默認(rèn)情況下HotSpot VM則會(huì)根據(jù)操作系統(tǒng)版本與物理機(jī)器的硬件性能自動(dòng)選擇運(yùn)行在哪一種模式下,以及采用哪一種即時(shí)編譯器。
對(duì)于32位Windows操作系統(tǒng),不論硬件什么配置都會(huì)默認(rèn)使用Client模式,可以執(zhí)行“java -server -version”命令,切換為Server模式,但已經(jīng)是Server模式的,不能切換為Client模式。對(duì)于32位其他類型的操作系統(tǒng),如果內(nèi)存配置為2GB或以上且CPU數(shù)量大于或等于2,默認(rèn)情況會(huì)以Server模式運(yùn)行,低于該配置依然使用Client模式。64位的操作系統(tǒng)只有Server模式。
對(duì)于開發(fā)人員來講,基本都是64位的操作系統(tǒng)了,因?yàn)?2位的內(nèi)存限制為4GB,顯得捉襟見肘?,F(xiàn)在生產(chǎn)環(huán)境上,基本上都是Server模式。所以我們只需要掌握Server模式即可,Client模式基本不會(huì)使用了。
6、AOT編譯器和Graal編譯器
JDK 9引入了AOT編譯器(Ahead Of Time Compiler,靜態(tài)提前編譯器)及AOT編譯工具jaotc。將所輸入的class文件轉(zhuǎn)換為機(jī)器碼,并存放至生成的動(dòng)態(tài)共享庫之中。
所謂AOT編譯,是與即時(shí)編譯相對(duì)立的一個(gè)概念。我們知道,即時(shí)編譯指的是在程序的運(yùn)行過程中,將字節(jié)碼轉(zhuǎn)換為可在硬件上直接運(yùn)行的機(jī)器碼,并部署至托管環(huán)境中的過程。而AOT編譯指的則是,在程序運(yùn)行之前,便將字節(jié)碼轉(zhuǎn)換為機(jī)器碼的過程,也就是說在程序運(yùn)行之前通過jaotc工具將class文件轉(zhuǎn)換為so文件。
AOT編譯的最大好處是JVM加載已經(jīng)預(yù)編譯成二進(jìn)制庫,可以直接執(zhí)行,無須通過解釋器執(zhí)行,不必等待即時(shí)編譯器的預(yù)熱,減少Java應(yīng)用給人帶來“第一次運(yùn)行慢”的不良體驗(yàn)。把編譯的本地機(jī)器碼保存到磁盤,不占用內(nèi)存,并可多次使用。但是破壞了Java“一次編譯,到處運(yùn)行”的特性,必須為不同硬件編譯對(duì)應(yīng)的發(fā)行包,降低了Java鏈接過程的動(dòng)態(tài)性,加載的代碼在編譯工作前就必須全部已知。
自JDK 10起,HotSpot又加入一個(gè)全新的即時(shí)編譯器——Graal編譯器。它的編譯效果在短短幾年內(nèi)就追平了C2編譯器,未來可期。目前,它還依然帶著“試驗(yàn)狀態(tài)”的標(biāo)簽,需要使用參數(shù)“-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler”去激活,才可以使用。
7、小結(jié)
講述了執(zhí)行引擎在JVM中起到的作用,執(zhí)行引擎充當(dāng)了將class文件中的內(nèi)容翻譯為機(jī)器語言的譯者,使得物理機(jī)器可以識(shí)別,進(jìn)而使得程序可以執(zhí)行。HotSpot VM中的執(zhí)行引擎同時(shí)存在解釋器和JIT編譯器,即代碼可以解釋執(zhí)行,也可以編譯執(zhí)行。從執(zhí)行效率上講,編譯執(zhí)行要比解釋執(zhí)行的效率高。從JVM啟動(dòng)時(shí)間來看,解釋器可以首先發(fā)揮作用,而不必等待JIT全部編譯完成后再執(zhí)行,這樣可以省去許多不必要的編譯時(shí)間編譯執(zhí)行。此外,是否需要啟動(dòng)JIT編譯器將字節(jié)碼直接編譯為對(duì)應(yīng)平臺(tái)的機(jī)器碼需要根據(jù)代碼被調(diào)用執(zhí)行的頻率而定,盡管如此,程序編譯執(zhí)行仍是未來的發(fā)展方向。
到此這篇關(guān)于JVM執(zhí)行引擎的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)JVM執(zhí)行引擎內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java客戶端Etcd官方倉庫jetcd中KeepAlive接口實(shí)現(xiàn)
這篇文章主要為大家介紹了java客戶端Etcd官方倉庫jetcd中KeepAlive接口實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,多多加薪2022-02-02SpringCloud Zuul和Gateway的實(shí)例代碼(搭建方式)
本文主要介紹了SpringCloudZuul和SpringCloudGateway的簡(jiǎn)單示例,SpringCloudGateway是推薦使用的API網(wǎng)關(guān)解決方案,基于SpringFramework5和ProjectReactor構(gòu)建,具有更高的性能和吞吐量2025-02-02MyBatis?Plus?導(dǎo)入IdType失敗的解決
這篇文章主要介紹了MyBatis?Plus?導(dǎo)入IdType失敗的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12利用Java寫一個(gè)學(xué)生管理系統(tǒng)
今天這篇文章就給給大家分享利用Java寫一個(gè)學(xué)生管理系統(tǒng)吧,先寫一個(gè)簡(jiǎn)單的用List來實(shí)現(xiàn)學(xué)生管理系統(tǒng):2021-09-09SpringBoot打包前重新拉取maven依賴的方法實(shí)現(xiàn)
在使用 Maven 構(gòu)建 Spring Boot 項(xiàng)目時(shí),如果希望在每次打包時(shí)都強(qiáng)制拉取依賴,可以通過以下方法實(shí)現(xiàn),本文給大家介紹了四種實(shí)現(xiàn)方法,并通過代碼講解的非常詳細(xì),需要的朋友可以參考下2024-12-12一文教會(huì)Java新手使用Spring?MVC中的查詢字符串和查詢參數(shù)
在使用springMVC框架構(gòu)建web應(yīng)用,客戶端常會(huì)請(qǐng)求字符串、整型、json等格式的數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于通過一文教會(huì)Java新手使用Spring?MVC中的查詢字符串和查詢參數(shù)的相關(guān)資料,需要的朋友可以參考下2024-01-01