溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何進(jìn)行JVM虛擬機(jī)中Java的編譯期優(yōu)化與運(yùn)行期優(yōu)化

發(fā)布時(shí)間:2021-10-23 17:37:34 來源:億速云 閱讀:111 作者:柒染 欄目:編程語言

這篇文章將為大家詳細(xì)講解有關(guān)如何進(jìn)行JVM虛擬機(jī)中Java的編譯期優(yōu)化與運(yùn)行期優(yōu)化,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。

java編譯期優(yōu)化

java語言的編譯期其實(shí)是一段不確定的操作過程,因?yàn)樗梢苑譃槿惥幾g過程:
1.前端編譯:把.java文件轉(zhuǎn)變?yōu)?/em>.class文件
2.后端編譯:把字節(jié)碼轉(zhuǎn)變?yōu)闄C(jī)器碼
3.靜態(tài)提前編譯:直接把.java文件編譯成本地機(jī)器代碼
從JDK1.3開始,虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)就把對性能的優(yōu)化集中到了后端的即時(shí)編譯中,這樣可以讓那些不是由Javac產(chǎn)生的Class文件(如JRuby、Groovy等語言的Class文件)也能享受到編譯期優(yōu)化所帶來的好處
*Java中即時(shí)編譯在運(yùn)行期的優(yōu)化過程對于程序運(yùn)行來說更重要,而前端編譯期在編譯期的優(yōu)化過程對于程序編碼來說關(guān)系更加密切    

早期(編譯期)優(yōu)化

早期編譯過程主要分為3個(gè)部分:1.解析與填充符號表過程:詞法、語法分析;填充符號表  2.插入式注解處理器的注解處理過程  3.語義分析與字節(jié)碼生成過程:標(biāo)注檢查、數(shù)據(jù)與控制流分析、解語法糖、字節(jié)碼生成
泛型與類型擦除

Java語言中的泛型只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)替換成原來的原生類型了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼

泛型擦除前的例子    
public static void main( String[] args )
{
    Map<String,String> map = new HashMap<String, String>();
    map.put("hello","你好");
    System.out.println(map.get("hello"));
}
泛型擦除后的例子    
public static void main( String[] args )
{
    Map map = new HashMap();
    map.put("hello","你好");
    System.out.println((String)map.get("hello"));
}
自動裝箱、拆箱與遍歷循環(huán)

自動裝箱、拆箱在編譯之后會被轉(zhuǎn)化成對應(yīng)的包裝和還原方法,如Integer.valueOf()與Integer.intValue(),而遍歷循環(huán)則把代碼還原成了迭代器的實(shí)現(xiàn),變長參數(shù)會變成數(shù)組類型的參數(shù)。
然而包裝類的“==”運(yùn)算在不遇到算術(shù)運(yùn)算的情況下不會自動拆箱,以及它們的equals()方法不處理數(shù)據(jù)轉(zhuǎn)型的關(guān)系。

條件編譯

Java語言也可以進(jìn)行條件編譯,方法就是使用條件為常量的if語句,它在編譯階段就會被“運(yùn)行”:

public static void main(String[] args) {
    if(true){
        System.out.println("block 1");
    }
    else{
        System.out.println("block 2");
    }
}
編譯后Class文件的反編譯結(jié)果:
public static void main(String[] args) {
    System.out.println("block 1");
}

只能是條件為常量的if語句,這也是Java語言的語法糖,根據(jù)布爾常量值的真假,編譯器會把分支中不成立的代碼塊消除掉

晚期(運(yùn)行期)優(yōu)化

解釋器與編譯器

Java程序最初是通過解釋器進(jìn)行解釋執(zhí)行的,當(dāng)程序需要迅速啟動和執(zhí)行時(shí),解釋器可以首先發(fā)揮作用,省去編譯時(shí)間,立即執(zhí)行;當(dāng)程序運(yùn)行后,隨著時(shí)間的推移,編譯期逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼,獲得更高的執(zhí)行效率。解釋執(zhí)行節(jié)約內(nèi)存,編譯執(zhí)行提升效率。 同時(shí),解釋器可以作為編譯器激進(jìn)優(yōu)化時(shí)的一個(gè)“逃生門”,讓編譯器根據(jù)概率選擇一些大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立,則通過逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行。

HotSpot虛擬機(jī)中內(nèi)置了兩個(gè)即時(shí)編譯器,分別稱為Client Compiler(C1編譯器)和Server Compiler(C2編譯器),默認(rèn)采用解釋器與其中一個(gè)編譯器直接配合的方式工作,使用哪個(gè)編譯器取決于虛擬機(jī)運(yùn)行的模式,也可以自己去指定。若強(qiáng)制虛擬機(jī)運(yùn)行與“解釋模式”,編譯器完全不介入工作,若強(qiáng)制虛擬機(jī)運(yùn)行于“編譯模式”,則優(yōu)先采用編譯方式執(zhí)行程序,解釋器仍然要在編譯無法進(jìn)行的情況下介入執(zhí)行過程。
如何進(jìn)行JVM虛擬機(jī)中Java的編譯期優(yōu)化與運(yùn)行期優(yōu)化

分層編譯策略
分層編譯策略作為默認(rèn)編譯策略在JDK1.7的Server模式虛擬機(jī)中被開啟,其中包括:
第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能,可觸發(fā)第1層編譯;
第1層:C1編譯,將字節(jié)碼編譯成本地代碼,進(jìn)行簡單可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯;
第2層:C2編譯,也是將字節(jié)碼編譯成本地代碼,但是會啟動一些編譯耗時(shí)較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
實(shí)施分層編譯后,C1和C2將會同時(shí)工作,C1獲取更高的編譯速度,C2獲取更好的編譯質(zhì)量,在解釋執(zhí)行的時(shí)候也無須再承擔(dān)性能監(jiān)控信息的任務(wù)。
熱點(diǎn)代碼探測
在運(yùn)行過程中會被即時(shí)編譯器編譯的“熱點(diǎn)代碼”有兩類:
1.被多次調(diào)用的方法:由方法調(diào)用觸發(fā)的編譯,屬于JIT編譯方式
2.被多次執(zhí)行的循環(huán)體:也以整個(gè)方法作為編譯對象,因?yàn)榫幾g發(fā)生在方法執(zhí)行過程中,因此成為棧上替換(OSR編譯)
熱點(diǎn)探測判定方式有兩種:
1.基于采樣的熱點(diǎn)探測:虛擬機(jī)周期性的檢查各個(gè)線程的棧頂,如果某個(gè)方法經(jīng)常出現(xiàn)在棧頂,則判定為“熱點(diǎn)方法”。(簡單高效,可以獲取方法的調(diào)用關(guān)系,但容易受線程阻塞或別的外界因素影響擾亂熱點(diǎn)探測)
2.基于計(jì)數(shù)的熱點(diǎn)探測:虛擬機(jī)為每個(gè)方法建立一個(gè)計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),超過一定閾值就是“熱點(diǎn)方法”。(需要為每個(gè)方法維護(hù)計(jì)數(shù)器,不能直接獲取方法的調(diào)用關(guān)系,但是統(tǒng)計(jì)結(jié)果精確嚴(yán)謹(jǐn))

HotSpot虛擬機(jī)使用的是第二種,它為每個(gè)方法準(zhǔn)備了兩類計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器,下圖表示方法調(diào)用計(jì)數(shù)器觸發(fā)即時(shí)編譯:
如何進(jìn)行JVM虛擬機(jī)中Java的編譯期優(yōu)化與運(yùn)行期優(yōu)化

如果不做任何設(shè)置,執(zhí)行引擎會繼續(xù)進(jìn)入解釋器按照解釋方式執(zhí)行字節(jié)碼,直到提交的請求被編譯器編譯完成,下次調(diào)用才會使用已編譯的版本。另外,方法調(diào)用計(jì)數(shù)器的值也不是一個(gè)絕對次數(shù),而是一段時(shí)間之內(nèi)被調(diào)用的次數(shù),超過這個(gè)時(shí)間,次數(shù)就減半,這稱為計(jì)數(shù)器熱度的衰減。

下圖表示回邊計(jì)數(shù)器觸發(fā)即時(shí)編譯:
如何進(jìn)行JVM虛擬機(jī)中Java的編譯期優(yōu)化與運(yùn)行期優(yōu)化

回邊計(jì)數(shù)器沒有計(jì)數(shù)器熱度衰減的過程,因此統(tǒng)計(jì)的就是絕對次數(shù),并且當(dāng)計(jì)數(shù)器溢出時(shí),它還會把方法計(jì)數(shù)器的值也調(diào)整到溢出狀態(tài),這樣下次進(jìn)入該方法的時(shí)候就會執(zhí)行標(biāo)準(zhǔn)編譯過程。

編譯優(yōu)化技術(shù)

虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)幾乎把對代碼的所有優(yōu)化措施都集中在了即時(shí)編譯器之中,那么在編譯器編譯的過程中,到底做了些什么事情呢?下面將介紹幾種最有代表性的優(yōu)化技術(shù):
公共子表達(dá)式消除
如果一個(gè)表達(dá)式E已經(jīng)計(jì)算過了,并且先前的計(jì)算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就成為了公共表達(dá)式,可以直接用之前的結(jié)果替換。
例:int d = (c b) 12 + a + (a + b c) => int d = E 12 + a + (a + E)

數(shù)組邊界檢查消除
Java語言中訪問數(shù)組元素都要進(jìn)行上下界的范圍檢查,每次讀寫都有一次條件判定操作,這無疑是一種負(fù)擔(dān)。編譯器只要通過數(shù)據(jù)流分析就可以判定循環(huán)變量的取值范圍永遠(yuǎn)在數(shù)組長度以內(nèi),那么整個(gè)循環(huán)中就可以把上下界檢查消除,這樣可以省很多次的條件判斷操作。

另一種方法叫做隱式異常處理,Java中空指針的判斷和算術(shù)運(yùn)算中除數(shù)為0的檢查都采用了這個(gè)思路:

if(foo != null){
    return foo.value;
}else{
    throw new NullPointException();
}
使用隱式異常優(yōu)化以后:
try{
    return foo.value;
}catch(segment_fault){
    uncommon_trap();
}
當(dāng)foo極少為空時(shí),隱式異常優(yōu)化是值得的,但是foo經(jīng)常為空,這樣的優(yōu)化反而會讓程序變慢,而HotSpot虛擬機(jī)會根據(jù)運(yùn)行期收集到的Profile信息自動選擇最優(yōu)方案。

方法內(nèi)聯(lián)
方法內(nèi)聯(lián)能去除方法調(diào)用的成本,同時(shí)也為其他優(yōu)化建立了良好的基礎(chǔ),因此各種編譯器一般會把內(nèi)聯(lián)優(yōu)化放在優(yōu)化序列的最靠前位置,然而由于Java對象的方法默認(rèn)都是虛方法,因此方法調(diào)用都需要在運(yùn)行時(shí)進(jìn)行多態(tài)選擇,為了解決虛方法的內(nèi)聯(lián)問題,首先引入了“類型繼承關(guān)系分析(CHA)”的技術(shù)。

1.在內(nèi)聯(lián)時(shí),若是非虛方法,則可以直接內(nèi)聯(lián)  
2.遇到虛方法,首先根據(jù)CHA判斷此方法是否有多個(gè)目標(biāo)版本,若只有一個(gè),可以直接內(nèi)聯(lián),但是需要預(yù)留一個(gè)“逃生門”,稱為守護(hù)內(nèi)聯(lián),若在程序的后續(xù)執(zhí)行過程中,加載了導(dǎo)致繼承關(guān)系發(fā)生變化的新類,就需要拋棄已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新編譯。
3.若CHA判斷此方法有多個(gè)目標(biāo)版本,則編譯器會使用“內(nèi)聯(lián)緩存”,第一次調(diào)用緩存記錄下方法接收者的版本信息,并且每次調(diào)用都比較版本,若一致則可以一直使用,若不一致則取消內(nèi)聯(lián),查找虛方法表進(jìn)行方法分派。

逃逸分析
逃逸分析的基本行為就是分析對象動態(tài)作用域,當(dāng)一個(gè)對象被外部方法所引用,稱為方法逃逸;當(dāng)被外部線程訪問,稱為線程逃逸。若能證明一個(gè)對象不會被外部方法或進(jìn)程引用,則可以為這個(gè)變量進(jìn)行一些優(yōu)化:

1.棧上分配:如果確定一個(gè)對象不會逃逸,則可以讓它分配在棧上,對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀。這樣可以減小垃圾收集系統(tǒng)的壓力。  
2.同步消除:線程同步相對耗時(shí),如果確定一個(gè)變量不會逃逸出線程,那這個(gè)變量的讀寫不會有競爭,則對這個(gè)變量實(shí)施的同步措施也就可以消除掉。  
3.標(biāo)量替換:如果逃逸分析證明一個(gè)對象不會被外部訪問,并且這個(gè)對象可以被拆散的話,那么程序真正執(zhí)行的時(shí)候可以不創(chuàng)建這個(gè)對象,改為直接創(chuàng)建它的成員變量,這樣就可以在棧上分配。

可是目前還不能保證逃逸分析的性能收益必定高于它的消耗,所以這項(xiàng)技術(shù)還不是很成熟。

java與C/C++編譯器對比

Java虛擬機(jī)的即時(shí)編譯器與C/C++的靜態(tài)編譯器相比,可能會由于下面的原因?qū)е螺敵龅谋镜卮a有一些劣勢:
1.即時(shí)編譯器運(yùn)行占用的是用戶程序的運(yùn)行時(shí)間,具有很大的時(shí)間壓力,因此不敢隨便引入大規(guī)模的優(yōu)化技術(shù);
2.Java語言是動態(tài)的類型安全語言,虛擬器需要頻繁的進(jìn)行動態(tài)檢查,如空指針,上下界范圍,繼承關(guān)系等;
3.Java中使用虛方法頻率遠(yuǎn)高于C++,則需要進(jìn)行多態(tài)選擇的頻率遠(yuǎn)高于C++;
4.Java是可以動態(tài)擴(kuò)展的語言,運(yùn)行時(shí)加載新的類可能改變原有的繼承關(guān)系,許多全局的優(yōu)化措施只能以激進(jìn)優(yōu)化的方式來完成;
5.Java語言的對象內(nèi)存都在堆上分配,垃圾回收的壓力比C++大
然而,Java語言這些性能上的劣勢換取了開發(fā)效率上的優(yōu)勢,并且由于C++編譯器所有優(yōu)化都是在編譯期完成的,以運(yùn)行期性能監(jiān)控為基礎(chǔ)的優(yōu)化措施都無法進(jìn)行,這也是Java編譯器獨(dú)有的優(yōu)勢。

關(guān)于如何進(jìn)行JVM虛擬機(jī)中Java的編譯期優(yōu)化與運(yùn)行期優(yōu)化就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI