日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

java编译_解析 Java 即时编译器原理。

發(fā)布時間:2024/8/23 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java编译_解析 Java 即时编译器原理。 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
↑ 點擊上面 “時代Java”關(guān)注我們,關(guān)注新技術(shù),學習新知識!

一、導讀

常見的編譯型語言如C++,通常會把代碼直接編譯成CPU所能理解的機器碼來運行。而Java為了實現(xiàn)“一次編譯,處處運行”的特性,把編譯的過程分成兩部分,首先它會先由javac編譯成通用的中間形式——字節(jié)碼,然后再由解釋器逐條將字節(jié)碼解釋為機器碼來執(zhí)行。所以在性能上,Java通常不如C++這類編譯型語言。

為了優(yōu)化Java的性能 ,JVM在解釋器之外引入了即時(Just In Time)編譯器:當程序運行時,解釋器首先發(fā)揮作用,代碼可以直接執(zhí)行。隨著時間推移,即時編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯優(yōu)化成本地代碼,來獲取更高的執(zhí)行效率。解釋器這時可以作為編譯運行的降級手段,在一些不可靠的編譯優(yōu)化出現(xiàn)問題時,再切換回解釋執(zhí)行,保證程序可以正常運行。

即時編譯器極大地提高了Java程序的運行速度,而且跟靜態(tài)編譯相比,即時編譯器可以選擇性地編譯熱點代碼,省去了很多編譯時間,也節(jié)省很多的空間。目前,即時編譯器已經(jīng)非常成熟了,在性能層面甚至可以和編譯型語言相比。不過在這個領(lǐng)域,大家依然在不斷探索如何結(jié)合不同的編譯方式,使用更加智能的手段來提升程序的運行速度。

二、Java的執(zhí)行過程

Java的執(zhí)行過程整體可以分為兩個部分,第一步由javac將源碼編譯成字節(jié)碼,在這個過程中會進行詞法分析、語法分析、語義分析,編譯原理中這部分的編譯稱為前端編譯。接下來無需編譯直接逐條將字節(jié)碼解釋執(zhí)行,在解釋執(zhí)行的過程中,虛擬機同時對程序運行的信息進行收集,在這些信息的基礎(chǔ)上,編譯器會逐漸發(fā)揮作用,它會進行后端編譯——把字節(jié)碼編譯成機器碼,但不是所有的代碼都會被編譯,只有被JVM認定為的熱點代碼,才可能被編譯。

怎么樣才會被認為是熱點代碼呢?JVM中會設(shè)置一個閾值,當方法或者代碼塊的在一定時間內(nèi)的調(diào)用次數(shù)超過這個閾值時就會被編譯,存入codeCache中。當下次執(zhí)行時,再遇到這段代碼,就會從codeCache中讀取機器碼,直接執(zhí)行,以此來提升程序運行的性能。整體的執(zhí)行過程大致如下圖所示:

1. JVM中的編譯器

JVM中集成了兩種編譯器,Client Compiler和Server Compiler,它們的作用也不同。Client Compiler注重啟動速度和局部的優(yōu)化,Server Compiler則更加關(guān)注全局的優(yōu)化,性能會更好,但由于會進行更多的全局分析,所以啟動速度會變慢。兩種編譯器有著不同的應(yīng)用場景,在虛擬機中同時發(fā)揮作用。

Client Compiler

HotSpot VM帶有一個Client Compiler ?C1編譯器。這種編譯器啟動速度快,但是性能比較Server Compiler來說會差一些。C1會做三件事:

  • 局部簡單可靠的優(yōu)化,比如字節(jié)碼上進行的一些基礎(chǔ)優(yōu)化,方法內(nèi)聯(lián)、常量傳播等,放棄許多耗時較長的全局優(yōu)化。

  • 將字節(jié)碼構(gòu)造成高級中間表示(High-level Intermediate Representation,以下稱為HIR),HIR與平臺無關(guān),通常采用圖結(jié)構(gòu),更適合JVM對程序進行優(yōu)化。

  • 最后將HIR轉(zhuǎn)換成低級中間表示(Low-level Intermediate Representation,以下稱為LIR),在LIR的基礎(chǔ)上會進行寄存器分配、窺孔優(yōu)化(局部的優(yōu)化方式,編譯器在一個基本塊或者多個基本塊中,針對已經(jīng)生成的代碼,結(jié)合CPU自己指令的特點,通過一些認為可能帶來性能提升的轉(zhuǎn)換規(guī)則或者通過整體的分析,進行指令轉(zhuǎn)換,來提升代碼性能)等操作,最終生成機器碼。

Server Compiler

Server Compiler主要關(guān)注一些編譯耗時較長的全局優(yōu)化,甚至會還會根據(jù)程序運行的信息進行一些不可靠的激進優(yōu)化。這種編譯器的啟動時間長,適用于長時間運行的后臺程序,它的性能通常比Client Compiler高30%以上。目前,Hotspot虛擬機中使用的Server Compiler有兩種:C2和Graal。

C2 Compiler

在Hotspot VM中,默認的Server Compiler是C2編譯器。

C2編譯器在進行編譯優(yōu)化時,會使用一種控制流與數(shù)據(jù)流結(jié)合的圖數(shù)據(jù)結(jié)構(gòu),稱為Ideal Graph。Ideal Graph表示當前程序的數(shù)據(jù)流向和指令間的依賴關(guān)系,依靠這種圖結(jié)構(gòu),某些優(yōu)化步驟(尤其是涉及浮動代碼塊的那些優(yōu)化步驟)變得不那么復雜。

Ideal Graph的構(gòu)建是在解析字節(jié)碼的時候,根據(jù)字節(jié)碼中的指令向一個空的Graph中添加節(jié)點,Graph中的節(jié)點通常對應(yīng)一個指令塊,每個指令塊包含多條相關(guān)聯(lián)的指令,JVM會利用一些優(yōu)化技術(shù)對這些指令進行優(yōu)化,比如Global Value Numbering、常量折疊等,解析結(jié)束后,還會進行一些死代碼剔除的操作。生成Ideal Graph后,會在這個基礎(chǔ)上結(jié)合收集的程序運行信息來進行一些全局的優(yōu)化,這個階段如果JVM判斷此時沒有全局優(yōu)化的必要,就會跳過這部分優(yōu)化。

無論是否進行全局優(yōu)化,Ideal Graph都會被轉(zhuǎn)化為一種更接近機器層面的MachNode Graph,最后編譯的機器碼就是從MachNode Graph中得的,生成機器碼前還會有一些包括寄存器分配、窺孔優(yōu)化等操作。關(guān)于Ideal Graph和各種全局的優(yōu)化手段會在后面的章節(jié)詳細介紹。Server Compiler編譯優(yōu)化的過程如下圖所示:

Graal Compiler

從JDK 9開始,Hotspot VM中集成了一種新的Server Compiler,Graal編譯器。相比C2編譯器,Graal有這樣幾種關(guān)鍵特性:

  • 前文有提到,JVM會在解釋執(zhí)行的時候收集程序運行的各種信息,然后編譯器會根據(jù)這些信息進行一些基于預(yù)測的激進優(yōu)化,比如分支預(yù)測,根據(jù)程序不同分支的運行概率,選擇性地編譯一些概率較大的分支。Graal比C2更加青睞這種優(yōu)化,所以Graal的峰值性能通常要比C2更好。

  • 使用Java編寫,對于Java語言,尤其是新特性,比如Lambda、Stream等更加友好。

  • 更深層次的優(yōu)化,比如虛函數(shù)的內(nèi)聯(lián)、部分逃逸分析等。

Graal編譯器可以通過Java虛擬機參數(shù)-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler啟用。當啟用時,它將替換掉HotSpot中的C2編譯器,并響應(yīng)原本由C2負責的編譯請求。

2. 分層編譯

在Java 7以前,需要研發(fā)人員根據(jù)服務(wù)的性質(zhì)去選擇編譯器。對于需要快速啟動的,或者一些不會長期運行的服務(wù),可以采用編譯效率較高的C1,對應(yīng)參數(shù)-client。長期運行的服務(wù),或者對峰值性能有要求的后臺服務(wù),可以采用峰值性能更好的C2,對應(yīng)參數(shù)-server。Java 7開始引入了分層編譯的概念,它結(jié)合了C1和C2的優(yōu)勢,追求啟動速度和峰值性能的一個平衡。分層編譯將JVM的執(zhí)行狀態(tài)分為了五個層次。五個層級分別是:

  • 解釋執(zhí)行。

  • 執(zhí)行不帶profiling的C1代碼。

  • 執(zhí)行僅帶方法調(diào)用次數(shù)以及循環(huán)回邊執(zhí)行次數(shù)profiling的C1代碼。

  • 執(zhí)行帶所有profiling的C1代碼。

  • 執(zhí)行C2代碼。

profiling就是收集能夠反映程序執(zhí)行狀態(tài)的數(shù)據(jù)。其中最基本的統(tǒng)計數(shù)據(jù)就是方法的調(diào)用次數(shù),以及循環(huán)回邊的執(zhí)行次數(shù)。

通常情況下,C2代碼的執(zhí)行效率要比C1代碼的高出30%以上。C1層執(zhí)行的代碼,按執(zhí)行效率排序從高至低則是1層>2層>3層。這5個層次中,1層和4層都是終止狀態(tài),當一個方法到達終止狀態(tài)后,只要編譯后的代碼并沒有失效,那么JVM就不會再次發(fā)出該方法的編譯請求的。服務(wù)實際運行時,JVM會根據(jù)服務(wù)運行情況,從解釋執(zhí)行開始,選擇不同的編譯路徑,直到到達終止狀態(tài)。下圖中就列舉了幾種常見的編譯路徑:

  • 圖中第①條路徑,代表編譯的一般情況,熱點方法從解釋執(zhí)行到被3層的C1編譯,最后被4層的C2編譯。

  • 如果方法比較小(比如Java服務(wù)中常見的getter/setter方法),3層的profiling沒有收集到有價值的數(shù)據(jù),JVM就會斷定該方法對于C1代碼和C2代碼的執(zhí)行效率相同,就會執(zhí)行圖中第②條路徑。在這種情況下,JVM會在3層編譯之后,放棄進入C2編譯,直接選擇用1層的C1編譯運行。

  • 在C1忙碌的情況下,執(zhí)行圖中第③條路徑,在解釋執(zhí)行過程中對程序進行profiling ,根據(jù)信息直接由第4層的C2編譯。

  • 前文提到C1中的執(zhí)行效率是1層>2層>3層,第3層一般要比第2層慢35%以上,所以在C2忙碌的情況下,執(zhí)行圖中第④條路徑。這時方法會被2層的C1編譯,然后再被3層的C1編譯,以減少方法在3層的執(zhí)行時間。

  • 如果編譯器做了一些比較激進的優(yōu)化,比如分支預(yù)測,在實際運行時發(fā)現(xiàn)預(yù)測出錯,這時就會進行反優(yōu)化,重新進入解釋執(zhí)行,圖中第⑤條執(zhí)行路徑代表的就是反優(yōu)化。

總的來說,C1的編譯速度更快,C2的編譯質(zhì)量更高,分層編譯的不同編譯路徑,也就是JVM根據(jù)當前服務(wù)的運行情況來尋找當前服務(wù)的最佳平衡點的一個過程。從JDK 8開始,JVM默認開啟分層編譯。

3. 即時編譯的觸發(fā)

Java虛擬機根據(jù)方法的調(diào)用次數(shù)以及循環(huán)回邊的執(zhí)行次數(shù)來觸發(fā)即時編譯。循環(huán)回邊是一個控制流圖中的概念,程序中可以簡單理解為往回跳轉(zhuǎn)的指令,比如下面這段代碼:

循環(huán)回邊

public void nlp(Object obj) { int sum = 0; for (int i = 0; i < 200; i++) { sum += i; }}

上面這段代碼經(jīng)過編譯生成下面的字節(jié)碼。其中,偏移量為18的字節(jié)碼將往回跳至偏移量為4的字節(jié)碼中。在解釋執(zhí)行時,每當運行一次該指令,Java虛擬機便會將該方法的循環(huán)回邊計數(shù)器加1。

字節(jié)碼

public void nlp(java.lang.Object); Code: 0: iconst_0 1: istore_1 2: iconst_0 3: istore_2 4: iload_2 5: sipush 200 8: if_icmpge 21 11: iload_1 12: iload_2 13: iadd 14: istore_1 15: iinc 2, 1 18: goto 4 21: return

在即時編譯過程中,編譯器會識別循環(huán)的頭部和尾部。上面這段字節(jié)碼中,循環(huán)體的頭部和尾部分別為偏移量為11的字節(jié)碼和偏移量為15的字節(jié)碼。編譯器將在循環(huán)體結(jié)尾增加循環(huán)回邊計數(shù)器的代碼,來對循環(huán)進行計數(shù)。

當方法的調(diào)用次數(shù)和循環(huán)回邊的次數(shù)的和,超過由參數(shù)-XX:CompileThreshold指定的閾值時(使用C1時,默認值為1500;使用C2時,默認值為10000),就會觸發(fā)即時編譯。

開啟分層編譯的情況下,-XX:CompileThreshold參數(shù)設(shè)置的閾值將會失效,觸發(fā)編譯會由以下的條件來判斷:

  • 方法調(diào)用次數(shù)大于由參數(shù)-XX:TierXInvocationThreshold指定的閾值乘以系數(shù)。

  • 方法調(diào)用次數(shù)大于由參數(shù)-XX:TierXMINInvocationThreshold指定的閾值乘以系數(shù),并且方法調(diào)用次數(shù)和循環(huán)回邊次數(shù)之和大于由參數(shù)-XX:TierXCompileThreshold指定的閾值乘以系數(shù)時。

分層編譯觸發(fā)條件公式

i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s)i為調(diào)用次數(shù),b是循環(huán)回邊次數(shù)

上述滿足其中一個條件就會觸發(fā)即時編譯,并且JVM會根據(jù)當前的編譯方法數(shù)以及編譯線程數(shù)動態(tài)調(diào)整系數(shù)s。

--

知識分享,時代前行!

~~ 時代Java

還有更多好文章……

請查看歷史文章和官網(wǎng),

↓有分享,有收獲~

總結(jié)

以上是生活随笔為你收集整理的java编译_解析 Java 即时编译器原理。的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。