java編譯器,java解釋器
1.java程序是一種可跨平臺執(zhí)行的語言,之所以可以跨平臺,是因為jvm的存在,JVM屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息,使java文件只需要生成為jvm可識別的字節(jié)碼(*.class)文件,jvm會將字節(jié)碼文件交給解釋器,翻譯成機(jī)器碼,由解釋器執(zhí)行,JVM解釋執(zhí)行字節(jié)碼文件就是JVM操作Java解釋器進(jìn)行解釋執(zhí)行字節(jié)碼文件的過程。這樣保證了在任何平臺上,都可以成功的執(zhí)行java程序,jvm的存在就是java可跨平臺的核心。
2.解釋器是把高級語言一行一行直接翻譯再運行,它不會一次性把整個文件都翻譯過來,而是翻譯一句,執(zhí)行一句,再翻譯,再執(zhí)行,所以解釋器的程序運行起來會比較慢,每次都要解釋之后再執(zhí)行。
3.動態(tài)編譯(dynamic compilation)指的是“在運行時進(jìn)行編譯”;與之相對的是事前編譯(ahead-of-time compilation,簡稱AOT),也叫靜態(tài)編譯(static compilation)。
什么是JIT
1.JIT編譯(just-in-time?compilation)狹義來說是當(dāng)某段代碼即將第一次被執(zhí)行時進(jìn)行編譯,因而叫“即時編譯”。JIT編譯是動態(tài)編譯的一種特例。JIT編譯一詞后來被泛華,時常與動態(tài)編譯等價;但要注意廣義與狹義的JIT編譯所指的區(qū)別。JIT(即時編譯)是用來提高java程序運行效率的,原本字節(jié)碼由解釋器需要經(jīng)過解釋再運行,現(xiàn)在有了JIT技術(shù),將字節(jié)碼編譯成平臺相關(guān)的原生機(jī)器碼,并進(jìn)行各個層次的優(yōu)化,這些機(jī)器碼會被緩存起來,以備下次使用,如果JIT對每條字節(jié)碼都進(jìn)行編譯,緩存(緩存的指令是有限的),會增加開銷,因此JIT只對熱點代碼進(jìn)行即時編譯,如循環(huán),高頻度使用的方法,會將整個方法編譯成本地機(jī)器碼,然后直接運行機(jī)器碼。
java虛擬機(jī)并沒有規(guī)定一定要有JIT,但是,即時編譯器編譯性能的好壞、代碼優(yōu)化程度的高低卻是衡量一款商用虛擬機(jī)優(yōu)秀與否的最關(guān)鍵的指標(biāo)之一,它也是虛擬機(jī)中最核心且最能體現(xiàn)虛擬機(jī)技術(shù)水平的部分。
由于Java虛擬機(jī)規(guī)范并沒有具體的約束規(guī)則去限制即使編譯器應(yīng)該如何實現(xiàn),所以這部分功能完全是與虛擬機(jī)具體實現(xiàn)相關(guān)的內(nèi)容,如無特殊說明,我們提到的編譯器、即時編譯器都是指Hotspot虛擬機(jī)內(nèi)的即時編譯器,虛擬機(jī)也是特指HotSpot虛擬機(jī)。

為什么HotSpot要使用編譯器和解釋器并存的模式
1.盡管并不是所有的Java虛擬機(jī)都采用解釋器與編譯器并存的架構(gòu),但許多主流的商用虛擬機(jī)(如HotSpot),都同時包含解釋器和編譯器。解釋器與編譯器兩者各有優(yōu)勢:當(dāng)程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。在程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲取更高的執(zhí)行效率。當(dāng)程序運行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)中),可以使用解釋器執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率。此外,如果編譯后出現(xiàn)“罕見陷阱”,可以通過逆優(yōu)化退回到解釋執(zhí)行。
2.編譯器的時間開銷和空間開銷:
解釋器的執(zhí)行,抽象的看是這樣的:
字節(jié)碼 -> [ 解釋器 解釋執(zhí)行機(jī)器碼 ] -> 執(zhí)行結(jié)果
而要JIT編譯然后再執(zhí)行的話,抽象的看則是:
字節(jié)碼 -> [ 編譯器 編譯 ] -> 與機(jī)器相關(guān)的機(jī)器碼-> [ 執(zhí)行 ] -> 執(zhí)行結(jié)果
說JIT比解釋快,其實說的是“執(zhí)行編譯后的代碼”比“解釋器解釋執(zhí)行”要快,并不是說“編譯”這個動作比“解釋”這個動作快。
JIT編譯再怎么快,至少也比解釋執(zhí)行一次略慢一些,而要得到最后的執(zhí)行結(jié)果還得再經(jīng)過一個“執(zhí)行編譯后的代碼”的過程。所以,對“只執(zhí)行一次”的代碼而言,解釋執(zhí)行其實總是比JIT編譯執(zhí)行要快。怎么算是“只執(zhí)行一次的代碼”呢?粗略說,下面兩個條件同時滿足時就是嚴(yán)格的“只執(zhí)行一次”
1、只被調(diào)用一次,例如類的構(gòu)造器(class initializer,())
2、沒有循環(huán)
對只執(zhí)行一次的代碼做JIT編譯再執(zhí)行,可以說是得不償失。對只執(zhí)行少量次數(shù)的代碼,JIT編譯帶來的執(zhí)行速度的提升也未必能抵消掉最初編譯帶來的開銷。只有對頻繁執(zhí)行的代碼,JIT編譯才能保證有正面的收益。
對一般的Java方法而言,編譯后代碼的大小相對于字節(jié)碼的大小,膨脹比達(dá)到10x是很正常的。同上面說的時間開銷一樣,這里的空間開銷也是,只有對執(zhí)行頻繁的代碼才值得編譯,如果把所有代碼都編譯則會顯著增加代碼所占空間,導(dǎo)致“代碼爆炸”。這也就解釋了為什么有些JVM會選擇不總是做JIT編譯,而是選擇用解釋器+JIT編譯器的混合執(zhí)行引擎。
哪些程序代碼會被編譯為本地代碼?如何編譯為本地代碼?
程序中的代碼只有是熱點代碼時,才會編譯為本地代碼,那么什么是熱點代碼呢?運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:
1、被多次調(diào)用的方法。
2、被多次執(zhí)行的循環(huán)體。
兩種情況,編譯器都是以整個方法作為編譯對象。 這種編譯方法因為編譯發(fā)生在方法執(zhí)行過程之中,因此形象的稱之為棧上替換(On Stack Replacement,OSR),即方法棧幀還在棧上,方法就被替換了。
如何判斷方法或一段代碼或是不是熱點代碼呢?
在HotSpot虛擬機(jī)中使用的基于計數(shù)器的熱點探測方法,因此它為每個方法準(zhǔn)備了兩個計數(shù)器:方法調(diào)用計數(shù)器和回邊計數(shù)器。在確定虛擬機(jī)運行參數(shù)的前提下,這兩個計數(shù)器都有一個確定的閾值,當(dāng)計數(shù)器超過閾值溢出了,就會觸發(fā)JIT編譯。
方法調(diào)用計數(shù)器:顧名思義,這個計數(shù)器用于統(tǒng)計方法被調(diào)用的次數(shù)。
當(dāng)一個方法被調(diào)用時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行。如果不存在已被編譯過的版本,則將此方法的調(diào)用計數(shù)器值加1,然后判斷方法調(diào)用計數(shù)器與回邊計數(shù)器值之和是否超過方法調(diào)用計數(shù)器的閾值。如果超過閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求。
如果不做任何設(shè)置,執(zhí)行引擎并不會同步等待編譯請求完成,而是繼續(xù)進(jìn)行解釋器按照解釋方式執(zhí)行字節(jié)碼,直到提交的請求被編譯器編譯完成。當(dāng)編譯工作完成之后,這個方法的調(diào)用入口地址就會系統(tǒng)自動改寫成新的,下一次調(diào)用該方法時就會使用已編譯的版本。
回變計數(shù)器:它的作用就是統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”。
六.如何編譯為本地代碼
Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的。
對Client Compiler來說,它是一個簡單快速的編譯器,主要關(guān)注點在于局部優(yōu)化,而放棄許多耗時較長的全局優(yōu)化手段。
而Server Compiler則是專門面向服務(wù)器端的,并為服務(wù)端的性能配置特別調(diào)整過的編譯器,是一個充分優(yōu)化過的高級編譯器。
1、Client Compiler
簡稱C1編譯器;
(A)應(yīng)用特點
較為輕量,只做少量性能開銷比較高的優(yōu)化,它占用內(nèi)存較少,適合于桌面交互式應(yīng)用。
(B)優(yōu)化技術(shù)
它是一個簡單快速的三段式編譯器,主要關(guān)注點在于局部性的優(yōu)化,而放棄了許多耗時較長的全局優(yōu)化;在寄存器分配策略上,JDK6以后采用的為線性掃描寄存器分配算法,其他方面的優(yōu)化,主要有方法內(nèi)聯(lián)、去虛擬化、冗余消除等;
(C)設(shè)置參數(shù)
可以使用"-client"參數(shù)強(qiáng)制選擇運行在Client模式(Client VM)
2、Server Compiler
簡稱C2編譯器,也叫Opto編譯器;
(A)應(yīng)用特點
較為重量,采用了大量傳統(tǒng)編譯優(yōu)化的技巧來進(jìn)行優(yōu)化,占用內(nèi)存相對多一些,適合服務(wù)器端的應(yīng)用。
(B)優(yōu)化技術(shù)
它會執(zhí)行所有經(jīng)典的優(yōu)化動作,如無用代碼消除、循環(huán)展開、循環(huán)表達(dá)式外提、消除公表達(dá)式、常量傳播、基本塊重排序等;還會一些與Java語言特性密切相關(guān)的優(yōu)化技術(shù),如范圍檢查消除、空值檢查消除等;另外,還進(jìn)行一些不穩(wěn)定的激進(jìn)優(yōu)化,如守護(hù)內(nèi)聯(lián)、分支頻率預(yù)測等。
(C)收集性能信息
由于C2會收集程序運行信息,因此其優(yōu)化范圍更多在于全局優(yōu)化,不僅僅是一個方塊的優(yōu)化;收集的信息主要有:分支的跳轉(zhuǎn)/不跳轉(zhuǎn)的頻率、某條指令上出現(xiàn)過的類型、是否出現(xiàn)過空值、是否出現(xiàn)過異常等。
(D)與C1的不同點
和C1的不同主要在于寄存器分配策略及優(yōu)化范圍,寄存器分配策略上C2采用傳統(tǒng)的全局圖著色寄存器分配算法;C2編譯速度較為緩慢,但遠(yuǎn)遠(yuǎn)超過傳統(tǒng)的靜態(tài)優(yōu)化編譯器;而且編譯輸出的代碼質(zhì)量高,可以減少本地代碼的執(zhí)行時間。
(E)設(shè)置參數(shù)
可以使用"-server"參數(shù)強(qiáng)制選擇運行在Server模式(Server VM)
七.HotSpot 虛擬機(jī)的優(yōu)化技術(shù)
1.HotSpot 虛擬機(jī)使用了很多種優(yōu)化技術(shù),這里只簡單介紹其中的幾種,完整的優(yōu)化技術(shù)介紹可以參考官網(wǎng)內(nèi)容。
(1)公共子表達(dá)式消除:
如果一個表達(dá)式已經(jīng)進(jìn)行過計算,并且在下次用到之前依賴的變量沒有變化,即表達(dá)式的計算結(jié)果不會發(fā)生變化,則在下次使用這個表達(dá)式時直接使用計算的結(jié)果。
(2)數(shù)組邊界檢查消除:
在 Java 中訪問數(shù)組時,會自動進(jìn)行邊界檢查來防止數(shù)組下標(biāo)越界。但是對于某些情況并不需要每次訪問都去檢查,如在一個循環(huán)中遍歷數(shù)組元素,如果虛擬機(jī)能夠確定下標(biāo)不會發(fā)生越界并且優(yōu)化確實能夠提高運行速度,則虛擬機(jī)會去除每次訪問的下標(biāo)檢查。
(3)方法內(nèi)聯(lián):
對于可以內(nèi)聯(lián)的方法,直接復(fù)制到調(diào)用者代碼中,減少方法調(diào)用次數(shù)和性能消耗。
(4)逃逸分析:
方法中定義的一個對象,如果會被其他方法訪問則稱為方法逃逸,如果會被其他線程訪問則稱為線程逃逸。對于不能逃逸的對象,HotSpot 虛擬機(jī)采用了棧上分配、同步消除、標(biāo)量替換等方法進(jìn)行優(yōu)化。
八.JIT編譯閾值
即時編譯JIT只在代碼段執(zhí)行足夠次數(shù)才會進(jìn)行優(yōu)化,在執(zhí)行過程中不斷收集各種數(shù)據(jù),作為優(yōu)化的決策,所以在優(yōu)化完成之前,例子中的User對象還是在堆上進(jìn)行分配。
那么一段代碼需要執(zhí)行多少次才會觸發(fā)JIT優(yōu)化呢?通常這個值由-XX:CompileThreshold參數(shù)進(jìn)行設(shè)置:
1、使用client編譯器時,默認(rèn)為1500;
2、使用server編譯器時,默認(rèn)為10000;
意味著如果方法調(diào)用次數(shù)或循環(huán)次數(shù)達(dá)到這個閾值就會觸發(fā)標(biāo)準(zhǔn)編譯,更改CompileThreshold標(biāo)志的值,將使編譯器提早(或延遲)編譯。
除了標(biāo)準(zhǔn)編譯,還有一個叫做OSR(On Stack Replacement)棧上替換的編譯,如上述例子中的main方法,只執(zhí)行一次,遠(yuǎn)遠(yuǎn)達(dá)不到閾值,但是方法體中執(zhí)行了多次循環(huán),OSR編譯就是只編譯該循環(huán)代碼,然后將其替換,下次循環(huán)時就執(zhí)行編譯好的代碼,不過觸發(fā)OSR編譯也需要一個閾值,可以通過以下公式得到。
-XX:CompileThreshold = 10000
-XX:OnStackReplacePercentage = 140
-XX:InterpreterProfilePercentage = 33
OSR trigger = (CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)) / 100 = 10700其中trigger即為OSR編譯的閾值。
更多關(guān)于編譯閾值信息請參考http://www.itdecent.cn/p/20bd2e9b1f03