溫馨提示×

溫馨提示×

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

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

怎么理解Java即時編譯

發(fā)布時間:2021-11-15 16:03:45 來源:億速云 閱讀:135 作者:iii 欄目:大數(shù)據(jù)

這篇文章主要介紹“怎么理解Java即時編譯”,在日常操作中,相信很多人在怎么理解Java即時編譯問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么理解Java即時編譯”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

簡介

當 JVM 的初始化完成后,類在調(diào)用執(zhí)行過程中,執(zhí)行引擎會把字節(jié)碼轉(zhuǎn)為機器碼,然后在操作系統(tǒng)中才能執(zhí)行。在字節(jié)碼轉(zhuǎn)換為機器碼的過程中,虛擬機中還存在著一道編譯,那就是即時編譯。

最初,JVM 中的字節(jié)碼是由解釋器( Interpreter )完成編譯的,當虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁的時候,就會把這些代碼認定為熱點代碼

為了提高熱點代碼的執(zhí)行效率,在運行時,即時編譯器(JIT,Just In Time)會把這些代碼編譯成與本地平臺相關的機器碼,并進行各層次的優(yōu)化,然后保存到內(nèi)存中。

分類

在 HotSpot 虛擬機中,內(nèi)置了兩種 JIT,分別為C1 編譯器C2 編譯器,這兩個編譯器的編譯過程是不一樣的。

C1 編譯器

C1 編譯器是一個簡單快速的編譯器,主要的關注點在于局部性的優(yōu)化,適用于執(zhí)行時間較短或?qū)有阅苡幸蟮某绦?,也稱為Client Compiler,例如,GUI 應用對界面啟動速度就有一定要求。

C2 編譯器

C2 編譯器是為長期運行的服務器端應用程序做性能調(diào)優(yōu)的編譯器,適用于執(zhí)行時間較長或?qū)Ψ逯敌阅苡幸蟮某绦颍卜Q為Server Compiler,例如,服務器上長期運行的 Java 應用對穩(wěn)定運行就有一定的要求。

分層編譯

在 Java7 之前,需要根據(jù)程序的特性來選擇對應的 JIT,虛擬機默認采用解釋器和其中一個編譯器配合工作。

Java7 引入了分層編譯,這種方式綜合了 C1 的啟動性能優(yōu)勢和 C2 的峰值性能優(yōu)勢,我們也可以通過參數(shù) -client或者-server 強制指定虛擬機的即時編譯模式。

分層編譯將 JVM 的執(zhí)行狀態(tài)分為了 5 個層次:

> 第 0 層:程序解釋執(zhí)行,默認開啟性能監(jiān)控功能(Profiling),如果不開啟,可觸發(fā)第二層編譯; > > 第 1 層:可稱為 C1 編譯,將字節(jié)碼編譯為本地代碼,進行簡單、可靠的優(yōu)化,不開啟 Profiling; > > 第 2 層:也稱為 C1 編譯,開啟 Profiling,僅執(zhí)行帶方法調(diào)用次數(shù)和循環(huán)回邊執(zhí)行次數(shù) profiling 的 C1 編譯; > > 第 3 層:也稱為 C1 編譯,執(zhí)行所有帶 Profiling 的 C1 編譯; > > 第 4 層:可稱為 C2 編譯,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至會根據(jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化。

對于 C1 的三種狀態(tài),按執(zhí)行效率從高至低:第 1 層、第 2層、第 3層。

通常情況下,C2 的執(zhí)行效率比 C1 高出30%以上。

在 Java8 中,默認開啟分層編譯,-client-server 的設置已經(jīng)是無效的了。如果只想開啟 C2,可以關閉分層編譯(-XX:-TieredCompilation),如果只想用 C1,可以在打開分層編譯的同時,使用參數(shù):-XX:TieredStopAtLevel=1

你可以通過 java -version命令行可以直接查看到當前系統(tǒng)使用的編譯模式:

C:\Users\Administrator>java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

mixed mode代表是默認的混合編譯模式,除了這種模式外,我們還可以使用-Xint參數(shù)強制虛擬機運行于只有解釋器的編譯模式下,這時 JIT 完全不介入工作;也可以使用參數(shù)-Xcomp強制虛擬機運行于只有 JIT 的編譯模式下。例如:

C:\Users\Administrator>java -Xint -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, interpreted mode)

C:\Users\Administrator>java -Xcomp -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, compiled mode)

觸發(fā)標準

在 HotSpot 虛擬機中,熱點探測是 JIT 的觸發(fā)標準。

> 熱點探測是基于計數(shù)器的熱點探測,采用這種方法的虛擬機會為每個方法建立計數(shù)器統(tǒng)計方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就認為它是“熱點方法” 。

虛擬機為每個方法準備了兩類計數(shù)器:方法調(diào)用計數(shù)器(Invocation Counter)和回邊計數(shù)器(Back Edge Counter)。在確定虛擬機運行參數(shù)的前提下,這兩個計數(shù)器都有一個確定的閾值,當計數(shù)器超過閾值溢出了,就會觸發(fā) JIT 編譯。

方法調(diào)用計數(shù)器

方法調(diào)用計數(shù)器用于統(tǒng)計方法被調(diào)用的次數(shù),默認閾值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通過-XX: CompileThreshold來設定;而在分層編譯的情況下-XX: CompileThreshold指定的閾值將失效,此時將會根據(jù)當前待編譯的方法數(shù)以及編譯線程數(shù)來動態(tài)調(diào)整。當方法計數(shù)器和回邊計數(shù)器之和超過方法計數(shù)器閾值時,就會觸發(fā) JIT 編譯器。

回邊計數(shù)器

回邊計數(shù)器用于統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”(Back Edge),該值用于計算是否觸發(fā) C1 編譯的閾值,在不開啟分層編譯的情況下,C1 默認為 13995,C2 默認為 10700,可通過 -XX: OnStackReplacePercentage=N來設置;而在分層編譯的情況下,-XX: OnStackReplacePercentage指定的閾值同樣會失效,此時將根據(jù)當前待編譯的方法數(shù)以及編譯線程數(shù)來動態(tài)調(diào)整。

建立回邊計數(shù)器的主要目的是為了觸發(fā) OSR(On StackReplacement)編譯,即棧上編譯。在一些循環(huán)周期比較長的代碼段中,當循環(huán)達到回邊計數(shù)器閾值時,JVM 會認為這段是熱點代碼,JIT 編譯器就會將這段代碼編譯成機器語言并緩存,在該循環(huán)時間段內(nèi),會直接將執(zhí)行代碼替換,執(zhí)行緩存的機器語言。

優(yōu)化技術

JIT 編譯運用了一些經(jīng)典的編譯優(yōu)化技術來實現(xiàn)代碼的優(yōu)化,即通過一些例行檢查優(yōu)化,可以智能地編譯出運行時的最優(yōu)性能代碼。主要有兩種:方法內(nèi)聯(lián)逃逸分析。

方法內(nèi)聯(lián)

調(diào)用一個方法通常要經(jīng)歷壓棧和出棧。調(diào)用方法是將程序執(zhí)行順序轉(zhuǎn)移到存儲該方法的內(nèi)存地址,將方法的內(nèi)容執(zhí)行完后,再返回到執(zhí)行該方法前的位置。

這種執(zhí)行操作要求在執(zhí)行前保護現(xiàn)場并記憶執(zhí)行的地址,執(zhí)行后要恢復現(xiàn)場,并按原來保存的地址繼續(xù)執(zhí)行。 因此,方法調(diào)用會產(chǎn)生一定的時間和空間方面的開銷(其實可以理解為一種上下文切換的精簡版)。

那么對于那些方法體代碼不是很大,又頻繁調(diào)用的方法來說,這個時間和空間的消耗會很大。

方法內(nèi)聯(lián)的優(yōu)化行為就是把目標方法的代碼復制到發(fā)起調(diào)用的方法之中,避免發(fā)生真實的方法調(diào)用。

JVM 會自動識別熱點方法,并對它們使用方法內(nèi)聯(lián)進行優(yōu)化。我們可以通過-XX:CompileThreshold來設置熱點方法的閾值。但要強調(diào)一點,熱點方法不一定會被 JVM 做內(nèi)聯(lián)優(yōu)化,如果這個方法體太大了,JVM 將不執(zhí)行內(nèi)聯(lián)操作。而方法體的大小閾值,我們也可以通過參數(shù)設置來優(yōu)化:

  1. 經(jīng)常執(zhí)行的方法,默認情況下,方法體大小小于 325 字節(jié)的都會進行內(nèi)聯(lián),我們可以通過-XX:MaxFreqInlineSize=N來設置大小值;

  2. 不是經(jīng)常執(zhí)行的方法,默認情況下,方法大小小于 35 字節(jié)才會進行內(nèi)聯(lián),我們也可以通過-XX:MaxInlineSize=N來重置大小值。

之后我們就可以通過配置 JVM 參數(shù)來查看到方法被內(nèi)聯(lián)的情況:

// 在控制臺打印編譯過程信息
-XX:+PrintCompilation
// 解鎖對 JVM 進行診斷的選項參數(shù)。默認是關閉的,開啟后支持一些特定參數(shù)對 JVM 進行診斷
-XX:+UnlockDiagnosticVMOptions
// 將內(nèi)聯(lián)方法打印出來
-XX:+PrintInlining

熱點方法的優(yōu)化可以有效提高系統(tǒng)性能,一般我們可以通過以下幾種方式來提高方法內(nèi)聯(lián):

  1. 通過設置 JVM 參數(shù)來減小熱點閾值或增加方法體閾值,以便更多的方法可以進行內(nèi)聯(lián),但這種方法意味著需要占用更多地內(nèi)存;

  2. 在編程中,避免在一個方法中寫大量代碼,習慣使用小方法體;

  3. 盡量使用 final、private、static 關鍵字修飾方法,編碼方法因為繼承,會需要額外的類型檢查。

> 此處就聯(lián)系到了最開始提出的觀點,一個方法中的內(nèi)容越少,當該方法經(jīng)常被執(zhí)行時,則容易進行方法內(nèi)聯(lián),從而優(yōu)化性能。

逃逸分析

逃逸分析(Escape Analysis)是判斷一個對象是否被外部方法引用或外部線程訪問的分析技術,編譯器會根據(jù)逃逸分析的結(jié)果對代碼進行優(yōu)化。

可以通過JVM參數(shù)進行設置:

-XX:+DoEscapeAnalysis 開啟逃逸分析(jdk1.8 默認開啟)
-XX:-DoEscapeAnalysis 關閉逃逸分析

其具體優(yōu)化方法主要有三種:棧上分配鎖消除、標量替換。

棧上分配

在 Java 中默認創(chuàng)建一個對象是在堆中分配內(nèi)存的,而當堆內(nèi)存中的對象不再使用時,則需要通過垃圾回收機制回收,這個過程相對分配在棧中的對象的創(chuàng)建和銷毀來說,更消耗時間和性能。

這個時候,逃逸分析如果發(fā)現(xiàn)一個對象只在方法中使用,就會將對象分配在棧上。

但是,HotSpot 虛擬機目前的實現(xiàn)導致棧上分配實現(xiàn)比較復雜,可以說,在 HotSpot 中暫時沒有實現(xiàn)這項優(yōu)化,所以大家可能暫時無法體會到這種優(yōu)化(我看的資料顯示在 Java8 中還沒有實現(xiàn),如果大家有什么其他的發(fā)現(xiàn),歡迎留言)。

鎖消除

如果是在單線程環(huán)境下,其實完全沒有必要使用線程安全的容器,但就算使用了,因為不會有線程競爭,這個時候 JIT 編譯會對這個對象的方法鎖進行鎖消除。例如:

	public static String getString(String s1, String s2) {
		StringBuffer sb = new StringBuffer();
		sb.append(s1);
		sb.append(s2);
		return sb.toString();
		}

可以通過JVM參數(shù)進行設置:

-XX:+EliminateLocks 開啟鎖消除(jdk1.8 默認開啟)
-XX:-EliminateLocks 關閉鎖消除

標量替換

逃逸分析證明一個對象不會被外部訪問,如果這個對象可以被拆分的話,當程序真正執(zhí)行的時候可能不創(chuàng)建這個對象,而直接創(chuàng)建它的成員變量來代替。將對象拆分后,可以分配對象的成員變量在?;蚣拇嫫魃?,原本的對象就無需分配內(nèi)存空間了。這種編譯優(yōu)化就叫做標量替換。

例如:

	public void foo() {
		TestInfo info = new TestInfo();
		info.id = 1;
		info.count = 99;
		// to do something
	}

逃逸分析后,代碼會被優(yōu)化為:

	public void foo() {
		id = 1;
		count = 99;
		// to do something
	}

可以通過JVM參數(shù)進行設置:

-XX:+EliminateAllocations 開啟標量替換(jdk1.8 默認開啟)
-XX:-EliminateAllocations 關閉就可以了

到此,關于“怎么理解Java即時編譯”的學習就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

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

AI