更新時間:2021-06-02 15:18:25 來源:動力節(jié)點 瀏覽1192次
學(xué)習(xí)JVM相關(guān)的知識,必然繞不開即時編譯器,因為它太重要了。了解了它的基本原理及優(yōu)化手段,在編程過程中可以讓我們有種打開任督二脈的感覺。比如,很多朋友在面試當(dāng)中還會遇到這樣的問題:Java是基于編譯執(zhí)行還是基于解釋執(zhí)行?當(dāng)你了解了Java的即時編譯器,不僅能夠輕松回答上述問題,還能如數(shù)家珍的講出JVM在即時編譯器上采用的優(yōu)化技術(shù),而且在實踐過程中更深刻的理解代碼背后的原理。本文便帶大家全面的了解Java即時編譯器。
在部分的商用虛擬機中,比如HotSpot中,Java程序先通過解釋器(Interceptor)進行解釋執(zhí)行。這也是為什么稱Java是基于解釋執(zhí)行的原因。但當(dāng)虛擬機發(fā)現(xiàn)某塊代碼或方法運行的特別頻繁,便會將其標(biāo)記為“熱點代碼”(Hot Spot Code)。
針對熱點代碼,虛擬機會采用各種措施來提升其執(zhí)行效率,因為執(zhí)行比較頻繁,如果能夠提升其執(zhí)行效率,性價比還是比較高的。為此,在運行時,虛擬機會把這些代碼編譯成與本地平臺相關(guān)的機器碼,并進行各層次的深度優(yōu)化。而這些優(yōu)化操作便是通過編譯器來完成的,也稱作即使編譯器(Just In Time Compiler,簡稱JIT編譯器)。
因此,準(zhǔn)確的來說,像HotSpot等虛擬機,Java是基于解釋執(zhí)行和編譯執(zhí)行的。下面用一張圖來解釋該過程:

首先,我們需要知道并不是所有的Java虛擬機都采用解釋器與編譯器并存的架構(gòu),但許多主流的商用虛擬機(如HotSpot),都同時包含解釋器和編譯器。
既然即時編譯器進行了各層次的優(yōu)化,那么為什么Java還使用解釋器來“拖累”程序的性能呢?這是因為,解釋器與編譯器兩者各有優(yōu)勢:當(dāng)程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。當(dāng)程序運行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)中),可以使用解釋器執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率。此外,如果編譯后出現(xiàn)“罕見陷阱”,可以通過逆優(yōu)化退回到解釋執(zhí)行。
Java虛擬機運行時,解釋器和即時編譯器能夠相互協(xié)作,取長補短。無論采用解釋器進行解釋執(zhí)行,還是采用即使編譯器進行編譯執(zhí)行,最終字節(jié)碼都需要被轉(zhuǎn)換為對應(yīng)平臺的本地機器碼指令。某些服務(wù)并不看重啟動時間,而某些服務(wù)卻非常看重,這就需要采用解釋器與即時編譯器并存來換取一個平衡點。
我們可以從解釋器和編譯器的編譯時間開銷和編譯空間開銷兩方面進行對比。首先,看編譯的時間開銷。

我們所說的JIT比解釋器快,僅限于對“熱點代碼”編譯之后的代碼執(zhí)行起來要比解釋器解釋執(zhí)行的快。通過上圖可以看出,如果是只是單次執(zhí)行的代碼,JIT編譯比解釋器要多出一步“執(zhí)行編譯”,因此,只執(zhí)行一次時,JIT是要比解釋器慢的。只執(zhí)行一次的代碼通常包括只被調(diào)用一次的代碼(比如構(gòu)造器)、沒有循環(huán)的代碼等,此時使用JIT顯然得不償失。
其次,再來看看編譯空間方面的開銷。對一般的Java方法而言,編譯后代碼的大小相對于字節(jié)碼,膨脹比達到10倍是很正常的。只有對執(zhí)行頻繁的代碼才值得編譯,如果把所有代碼都編譯則會顯著增加代碼所占空間,導(dǎo)致“代碼爆炸”。這就是為什么有些JVM不會單一使用JIT編譯,而是選擇用解釋器+JIT編譯器的混合執(zhí)行引擎。
HotSpot虛擬機為了使用不同的應(yīng)用場景,內(nèi)置了兩個即時編譯器:Client Complier和Server Complier,簡稱為C1、C2編譯器,分別用在客戶端和服務(wù)端。Client Complier可獲取更高的編譯速度,Server Complier可獲取更好的編譯質(zhì)量。
JVM Server模式與client模式最主要的差別在于:-server模式啟動時,速度較慢,但是一旦運行起來后,性能將會有很大的提升。原因是:當(dāng)虛擬機運行在-client模式時,使用的是一個代號為C1的輕量級編譯器,而-server模式啟動的虛擬機采用相對重量級代號為C2的編譯器。C2比C1編譯器編譯的相對徹底,服務(wù)起來之后,性能更高。
默認情況下,使用C1還是C2編譯器,要取決于虛擬機運行的模式。HotSpot虛擬機會根據(jù)自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可以使用“-client”或“-server”參數(shù)去強制指定虛擬機運行在Client模式或Server模式。
目前主流的HotSpot虛擬機中默認是采用解釋器與其中一個編譯器配合的方式工作,這種配合稱作混合模式(Mixed Mode)。用戶可以使用參數(shù)-Xint強制虛擬機運行于“解釋模式”(Interpreted Mode),這時候編譯器完全不介入工作。使用-Xcomp強制虛擬機運行于“編譯模式”(Compiled Mode),這時將優(yōu)先采用編譯方式執(zhí)行,但是解釋器仍然要在編譯無法進行的情況下接入執(zhí)行過程。通過虛擬機java-version命令可以查看當(dāng)前默認的運行模式。

在上述示例中我們不僅能夠看到采用的模式為mixed mode,還能看到出采用的是Server模式。
上面解釋了JIT編譯器的基本功能,那么它是如何判斷熱點代碼的呢?判斷一段代碼是不是熱點代碼的行為,也叫熱點探測(Hot Spot Detection),通常有兩種方法:基于采樣的熱點探測和基于計數(shù)器的熱點探測(HotSpot使用此方式)。
基于采樣的熱點探測(Sample Based Hot Spot Detection):Java虛擬機會周期的對各個線程棧頂進行檢查,如果某些方法經(jīng)常出現(xiàn)在棧頂,會被定義為“熱點方法”。實現(xiàn)簡單、高效,很容易獲取方法調(diào)用關(guān)系。但很難確認方法的reduce,容易受到線程阻塞或其他外因擾亂。
基于計數(shù)器的熱點探測(Counter Based Hot Spot Detection):為每個方法(甚至是代碼塊)建立計數(shù)器,執(zhí)行次數(shù)超過閾值就認為是“熱點方法”。統(tǒng)計結(jié)果精確嚴謹,但實現(xiàn)麻煩,不能直接獲取方法的調(diào)用關(guān)系。
HotSpot虛擬機默認采用基于計數(shù)器的熱點探測,有兩種計數(shù)器:方法調(diào)用計數(shù)器和回邊計數(shù)器。當(dāng)計數(shù)器數(shù)值大于默認閾值或指定閾值時,方法或代碼塊會被編譯成本地代碼。
方法調(diào)用計數(shù)器,記錄方法調(diào)用的次數(shù)。Client模式默認閾值是1500次,在Server模式下是10000次,可以通過-XX:CompileThreadhold來設(shè)定。如果不做任何設(shè)置,方法調(diào)用計數(shù)器統(tǒng)計的并不是方法被調(diào)用的絕對次數(shù),而是一個相對的執(zhí)行頻率,即一段時間之內(nèi)的方法被調(diào)用的次數(shù)。當(dāng)超過一定的時間限度,但調(diào)用次數(shù)仍然未達到閾值,那么該方法的調(diào)用計數(shù)器就會被減半,稱為方法調(diào)用計數(shù)器熱度的衰減(Counter Decay),這段時間稱為此方法的統(tǒng)計半衰周期(Counter Half Life Time)。進行熱度衰減的動作是在虛擬機進行垃圾收集時順便進行的,可以使用虛擬機參數(shù)-XX:CounterHalfLifeTime參數(shù)設(shè)置半衰周期的時間,單位是秒。JIT編譯交互圖如下:

回邊計數(shù)器,統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù)。在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”(Back Edge),建立回邊計數(shù)器統(tǒng)的目的是為了觸發(fā)OSR編譯。計數(shù)器的閾值,HotSpot提供了-XX:BackEdgeThreshold來進行設(shè)置,但當(dāng)前的虛擬機實際上使用了-XX:OnStackReplacePercentage來間接調(diào)整閾值,計算公式如下:

與方法計數(shù)器不同,回邊計數(shù)器沒有計數(shù)熱度衰減的過程,因此統(tǒng)計的就是該方法循環(huán)執(zhí)行的絕對次數(shù)。當(dāng)計數(shù)器溢出時,它還會把方法計數(shù)器的值也調(diào)整到溢出狀態(tài),這樣下次再進入該方法的時候就會執(zhí)行標(biāo)準(zhǔn)編譯過程。
以上就是動力節(jié)點小編介紹的"Java編譯器有哪些用法",希望對大家有幫助,如有疑問,請在線咨詢,有專業(yè)老師隨時為您服務(wù)。