Java 面試——即時(shí)編譯( JIT )

當(dāng)我們?cè)趯懘a時(shí),一個(gè)方法內(nèi)部的行數(shù)自然是越少越好,這樣邏輯清晰、方便閱讀,其實(shí)好處遠(yuǎn)不止如此,通過即時(shí)編譯,甚至可以提高執(zhí)行時(shí)的性能,今天就讓我們好好來了解一下其中的原理。

簡介

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

最初,JVM 中的字節(jié)碼是由解釋器( Interpreter )完成編譯的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁的時(shí)候,就會(huì)把這些代碼認(rèn)定為 熱點(diǎn)代碼 。

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

分類

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

C1 編譯器

C1 編譯器是一個(gè)簡單快速的編譯器,主要的關(guān)注點(diǎn)在于局部性的優(yōu)化,適用于執(zhí)行時(shí)間較短或?qū)?dòng)性能有要求的程序,也稱為 Client Compiler ,例如,GUI 應(yīng)用對(duì)界面啟動(dòng)速度就有一定要求。

C2 編譯器

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

分層編譯

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

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

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

第 0 層:程序解釋執(zhí)行,默認(rèn)開啟性能監(jiān)控功能(Profiling),如果不開啟,可觸發(fā)第二層編譯;

第 1 層:可稱為 C1 編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單、可靠的優(yōu)化,不開啟 Profiling;

第 2 層:也稱為 C1 編譯,開啟 Profiling,僅執(zhí)行帶方法調(diào)用次數(shù)和循環(huán)回邊執(zhí)行次數(shù) profiling 的 C1 編譯;

第 3 層:也稱為 C1 編譯,執(zhí)行所有帶 Profiling 的 C1 編譯;

第 4 層:可稱為 C2 編譯,也是將字節(jié)碼編譯為本地代碼,但是會(huì)啟用一些編譯耗時(shí)較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。

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

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

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

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

C:\Users\Administrator>java -versionjava 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 代表是默認(rèn)的混合編譯模式,除了這種模式外,我們還可以使用 -Xint 參數(shù)強(qiáng)制虛擬機(jī)運(yùn)行于只有解釋器的編譯模式下,這時(shí) JIT 完全不介入工作;也可以使用參數(shù) -Xcomp 強(qiáng)制虛擬機(jī)運(yùn)行于只有 JIT 的編譯模式下。例如:

C:\Users\Administrator>java -Xint -versionjava 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 -versionjava 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ā)標(biāo)準(zhǔn)

在 HotSpot 虛擬機(jī)中, 熱點(diǎn)探測(cè) 是 JIT 的觸發(fā)標(biāo)準(zhǔn)。

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

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

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

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

回邊計(jì)數(shù)器

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

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

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

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

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

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

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

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

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

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

-XX:MaxFreqInlineSize=N-XX:MaxInlineSize=N

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

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

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

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

在編程中,避免在一個(gè)方法中寫大量代碼,習(xí)慣使用小方法體;

盡量使用 final、private、static 關(guān)鍵字修飾方法,編碼方法因?yàn)槔^承,會(huì)需要額外的類型檢查。

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

逃逸分析

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

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

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

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

棧上分配

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

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

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

鎖消除

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

publicstaticString getString(String s1,String s2) {StringBuffer sb= newStringBuffer();sb.append(s1);sb.append(s2);returnsb.toString();}

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

-XX:+EliminateLocks 開啟鎖消除(jdk1.8默認(rèn)開啟)-XX:-EliminateLocks 關(guān)閉鎖消除

標(biāo)量替換

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

例如:

publicvoidfoo(){ TestInfo info =newTestInfo(); info.id =1; info.count =99;// to do something}

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

publicvoidfoo(){ id =1; count =99;// to do something}

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

-XX:+EliminateAllocations 開啟標(biāo)量替換(jdk1.8默認(rèn)開啟)-XX:-EliminateAllocations 關(guān)閉就可以了

需要java學(xué)習(xí)路線圖的私信筆者“java”領(lǐng)取哦!另外喜歡這篇文章的可以給筆者點(diǎn)個(gè)贊,關(guān)注一下,每天都會(huì)分享Java相關(guān)文章!還有不定時(shí)的福利贈(zèng)送,包括整理的學(xué)習(xí)資料,面試題,源碼等~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 編譯簡介 根據(jù)完成任務(wù)不同,可以將編譯器的組成部分劃分為前端(Front End)與后端(Back End)。...
    QuinnSun閱讀 871評(píng)論 0 1
  • 部分的商用虛擬機(jī)中,Java程序最初是通過解釋器進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁時(shí),就會(huì)...
    胡二囧閱讀 940評(píng)論 0 1
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 16,056評(píng)論 3 83
  • java編譯器,java解釋器 1.java程序是一種可跨平臺(tái)執(zhí)行的語言,之所以可以跨平臺(tái),是因?yàn)閖vm的存在,J...
    rabbit_coding閱讀 7,124評(píng)論 2 17
  • 昨天和一朋友聊天,他問我一個(gè)問題:一個(gè)人一輩子最重要的能力是什么? 我想了很久,最終也沒有回答他的問題,我不知道怎...
    黑金公主閱讀 492評(píng)論 0 0

友情鏈接更多精彩內(nèi)容