自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從零開始理解 JVM 的 JIT 編譯機(jī)制

開發(fā)
文將深入探討 JVM 中的 JIT 編譯技術(shù),揭示其背后的原理和工作機(jī)制,并介紹如何通過(guò)配置和調(diào)優(yōu)來(lái)最大化應(yīng)用性能。

在現(xiàn)代軟件開發(fā)中,Java 語(yǔ)言因其跨平臺(tái)性和強(qiáng)大的生態(tài)系統(tǒng)而廣受歡迎。然而,性能一直是開發(fā)者關(guān)注的重點(diǎn)之一。為了提升 Java 應(yīng)用的運(yùn)行效率,Java 虛擬機(jī)(JVM)引入了多種優(yōu)化技術(shù),其中最引人注目的莫過(guò)于即時(shí)編譯(Just-In-Time Compilation,簡(jiǎn)稱 JIT)。本文將深入探討 JVM 中的 JIT 編譯技術(shù),揭示其背后的原理和工作機(jī)制,并介紹如何通過(guò)配置和調(diào)優(yōu)來(lái)最大化應(yīng)用性能。

一、詳解JIT編譯技術(shù)

1.即時(shí)編譯的執(zhí)行點(diǎn)

在初始化階段完成后,執(zhí)行引擎不斷將調(diào)用到的字節(jié)碼翻譯成機(jī)器碼交由計(jì)算機(jī)執(zhí)行,Java字節(jié)碼轉(zhuǎn)為機(jī)器碼之間還有一步轉(zhuǎn)換,我們稱之為既時(shí)編譯:

最初Java字節(jié)碼文件是直接通過(guò)解釋器( Interpreter )解釋為機(jī)器碼直接運(yùn)行的。對(duì)于某些執(zhí)行頻率比較頻繁的代碼,我們可以稱之為熱點(diǎn)代碼,JIT就會(huì)針對(duì)這些熱點(diǎn)代碼進(jìn)行相應(yīng)的優(yōu)化并緩存,以提升程序的運(yùn)行效率:

2.即時(shí)編譯器類型有哪些?

我們以HotSpot 虛擬機(jī)為例,該虛擬機(jī)內(nèi)置了兩個(gè)JIT編譯器,分別為:

  • C1編譯器:主要關(guān)注點(diǎn)在于局部性優(yōu)化,常用于那些執(zhí)行時(shí)間短,或者要求快速啟動(dòng)的應(yīng)用程序,例如GUI應(yīng)用程序。
  • C2編譯器:常用于長(zhǎng)期運(yùn)行且對(duì)峰值性能有高要求的服務(wù)器。

所以我們也稱C1編譯器和C2編譯器為Client Compiler或者Server Compiler。

在Java7 之前,需要根據(jù)程序的特性來(lái)選擇對(duì)應(yīng)的JIT,虛擬機(jī)默認(rèn)采用解釋器和其中一個(gè)編譯器配合工作。Java7 引入了分層編譯,這種方式綜合了C1 的啟動(dòng)性能優(yōu)勢(shì)和C2 的峰值性能優(yōu)勢(shì),我們也可以通過(guò)參數(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)行簡(jiǎ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í)較長(zhǎng)的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。

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

我們可以使用java -version查看當(dāng)前編譯的編譯模式,可以看到筆者服務(wù)器的JVM使用的就是混合編譯模式:

java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

當(dāng)然,如果我們想將編譯器模式改為解釋器模式,就可以鍵入下面這條命令:

java -Xint -version

如果我們想強(qiáng)制運(yùn)行JIT編譯模式,也可以使用

java -Xcomp -version

二、JIT的熱點(diǎn)探測(cè)

1..什么是JIT熱點(diǎn)探測(cè)

HotSpot 虛擬機(jī)判定熱點(diǎn)代碼是基于兩種計(jì)數(shù)器進(jìn)行的,分別是方法調(diào)用計(jì)數(shù)器(Invocation Counter)和回邊計(jì)數(shù)器(Back Edge Counter),只有執(zhí)行代碼符合他們的標(biāo)準(zhǔn)且達(dá)到他的設(shè)置的閾值時(shí)才會(huì)進(jìn)行JIT編譯優(yōu)化。

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

方法調(diào)用器會(huì)針對(duì)方法的執(zhí)行頻率進(jìn)行相應(yīng)的優(yōu)化,當(dāng)某個(gè)方法執(zhí)行次數(shù)超過(guò)閾值時(shí),就會(huì)觸發(fā)JIT編譯優(yōu)化,這個(gè)閾值我們可以通過(guò)jinfo查看:

 jinfo -flag CompileThreshold pid

以筆者某個(gè)java進(jìn)程為例,可以看到JVM設(shè)置的方法調(diào)用計(jì)數(shù)器判定是否是熱點(diǎn)代碼的條件為調(diào)用次數(shù)達(dá)到10000次:

-XX:CompileThreshold=10000

這也就意味著當(dāng)方法調(diào)用在一段時(shí)間(而非永久疊加)次數(shù)達(dá)到10000次的時(shí)候,就會(huì)提交一個(gè)編譯請(qǐng)求,后續(xù)執(zhí)行時(shí)都直接用緩存中的編譯后的機(jī)器碼直接運(yùn)行:

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

在字節(jié)碼遇到控制流后跳轉(zhuǎn)的操作我們稱之為回邊,回邊計(jì)數(shù)器判定代碼為熱點(diǎn)代碼的條件是一個(gè)代碼在循環(huán)體內(nèi)達(dá)到回邊計(jì)數(shù)器要求的閾值,而這個(gè)閾值我們也可以通過(guò)jinfo查看

jinfo -flag OnStackReplacePercentage pid

以筆者的進(jìn)程為例可以看到當(dāng)回邊次數(shù)達(dá)到140時(shí)也會(huì)執(zhí)行相應(yīng)的JIT優(yōu)化,即當(dāng)這段代碼被判定為熱點(diǎn)代碼時(shí),JVM就會(huì)進(jìn)行一種棧上編譯的優(yōu)化操作,它會(huì)將這段代碼編譯為最優(yōu)邏輯保存到本地內(nèi)存,在執(zhí)行循環(huán)體的期間,直接使用緩存中的機(jī)器碼:

-XX:OnStackReplacePercentage=140

注意:與方法計(jì)數(shù)器不同,回邊計(jì)數(shù)器沒(méi)有計(jì)數(shù)熱度衰減的過(guò)程,因此這個(gè)計(jì)數(shù)器統(tǒng)計(jì)的就是該方法循環(huán)執(zhí)行的絕對(duì)次數(shù)。

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

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

我們都知道方法調(diào)用會(huì)經(jīng)歷一個(gè)壓棧和出棧的操作,執(zhí)行調(diào)用方法時(shí)會(huì)將地址轉(zhuǎn)移到存儲(chǔ)該方法的起始地址上,待調(diào)用結(jié)束后,在返回原來(lái)的位置。 這就意味著一個(gè)方法調(diào)用另一個(gè)方法時(shí),就需要保存當(dāng)前方法執(zhí)行位置,棧上壓入被調(diào)用方法,執(zhí)行完成后,恢復(fù)現(xiàn)場(chǎng)繼續(xù)執(zhí)行之前執(zhí)行的方法。因此方法調(diào)用期間是有一定的時(shí)間和空間的開銷的。

所以JIT會(huì)對(duì)那些方法調(diào)用方法非常頻繁的代碼執(zhí)行方法內(nèi)聯(lián):

private int add1(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
    return x1 + x2;
}

最終會(huì)被優(yōu)化為如下,由此減少方法調(diào)用時(shí)壓棧和出棧的開銷:

private int add1(int x1, int x2, int x3, int x4) {
    return x1 + x2 + x3 + x4;
}

但是方法內(nèi)斂?jī)?yōu)化也是有條件的,除了必須是熱點(diǎn)代碼(達(dá)到XX:CompileThreshold的閾值)以外,還要達(dá)到以下要求:

  • 對(duì)于經(jīng)常執(zhí)行的方法,方法體要小于325字節(jié),這個(gè)字節(jié)數(shù)可以通過(guò)-XX:MaxFreqInlineSize=N來(lái)調(diào)整。
  • 對(duì)于不經(jīng)常執(zhí)行的方法,方法體要小于35字節(jié),這個(gè)字節(jié)數(shù)可以由-XX:MaxInlineSize=N 來(lái)調(diào)整。

我們不妨看一段代碼,可以看到add1執(zhí)行了1000000次

public class JVMJit {
    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            add1(1, 2, 3, 4);
        }
    }

    private static int add1(int i, int i1, int i2, int i3) {
        return i + i1 + i2 + i3;
    }


}

我們可以對(duì)這段程序添加這樣一段參數(shù):

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

他們的含義分別是:

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

可以看到這段代碼被判定為熱點(diǎn)代碼,說(shuō)明他已經(jīng)被JVM優(yōu)化了:

所以這就要求我們平時(shí)寫代碼時(shí):

  • 方法體盡可能小。
  • 盡可能使用private、final、static修飾,避免一些沒(méi)必要的類是否繼承等相關(guān)檢查。

2.棧上分配

在將棧上分配前,我們需要先了解一個(gè)叫逃逸分析(Escape Analysis)的技術(shù)。 逃逸分析就是判斷當(dāng)前操作的對(duì)象是否有被外部方法引用或外部線程訪問(wèn)的一種技術(shù),若逃逸分析判定當(dāng)前對(duì)象并沒(méi)有被其他引用或者線程使用到的話,某些機(jī)制就可以開始進(jìn)行優(yōu)化,比如我現(xiàn)在要說(shuō)的棧上分配。

我們都知道創(chuàng)建一個(gè)對(duì)象,都是在堆上分配的,假如這個(gè)對(duì)象使用封閉,GC就會(huì)將其回收,而創(chuàng)建和回收這一來(lái)一回的操作也是有一定開銷的。而棧則不一樣,它使用的引用或者各種變量隨著調(diào)用的結(jié)束就消亡。

而棧上分配就是抓住這一特點(diǎn),當(dāng)他經(jīng)過(guò)逃逸分析技術(shù)發(fā)現(xiàn)這個(gè)對(duì)象并沒(méi)有被外部引用且僅在當(dāng)前線程使用,那么它就會(huì)將該對(duì)象分配在棧上。如下面這樣一段代碼:

public static void main(String[] args) {
    for (int i = 0; i < 200000 ; i++) {
     getAge();
    }
}
 
public static int getAge(){
 Student person = new Student(" 小明 ",18,30);   
    return person.getAge();
}
 
static class Student {
    private String name;
    private int age;
   
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
...get set
}

但是,在HotSpot 中暫時(shí)沒(méi)有實(shí)現(xiàn)這項(xiàng)優(yōu)化。隨著即時(shí)編譯器的發(fā)展與逃逸分析技術(shù)的逐漸成熟,相信不久的將來(lái)HotSpot 也會(huì)實(shí)現(xiàn)這項(xiàng)優(yōu)化功能。

3.鎖消除

同樣在逃逸分析某些沒(méi)有被外部方法或者其他線程引用的情況下,會(huì)將某些鎖消除。例如下面這段代碼,實(shí)際上你在運(yùn)行時(shí)可以發(fā)現(xiàn)StringBuffer 和StringBuilder 性能上沒(méi)有什么區(qū)別,這正是因?yàn)殒i消除為我們做的優(yōu)化工作。

  public static void main(String[] args) {
        appendStr(1000);
    }

    public static void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }

從編譯后的字節(jié)碼可以看出,因?yàn)閷?duì)象沒(méi)有發(fā)生逃逸,中間字符串拼接操作都是通過(guò)StringBuilder完成操作的,在StringBuilder完成字符串拼接之后再追加到StringBuffer上:

4.標(biāo)量替換

當(dāng)一個(gè)代碼的對(duì)象在方法上可以拆分,并且代碼僅僅是對(duì)這個(gè)對(duì)象的變量進(jìn)行各種操作的話,編譯器可能會(huì)執(zhí)行標(biāo)量替換,如下所示

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

由于上述代碼僅僅是創(chuàng)建一個(gè)對(duì)象后操作對(duì)象的變量,實(shí)際上這個(gè)工作似乎和對(duì)象沒(méi)有任何關(guān)聯(lián),編譯器識(shí)別到這點(diǎn)之后就不去創(chuàng)建沒(méi)必要的對(duì)象,進(jìn)而使用標(biāo)量替換的方式將對(duì)象的成員變量放到棧上,避免沒(méi)必要的對(duì)象創(chuàng)建和銷毀。

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

我們可以通過(guò)設(shè)置JVM 參數(shù)來(lái)開關(guān)逃逸分析,還可以單獨(dú)開關(guān)同步消除和標(biāo)量替換,在JDK1.8 中JVM 是默認(rèn)開啟這些操作的。

-XX:+DoEscapeAnalysis 開啟逃逸分析(jdk1.8 默認(rèn)開啟,其它版本未測(cè)試)
-XX:-DoEscapeAnalysis 關(guān)閉逃逸分析
 
-XX:+EliminateLocks 開啟鎖消除(jdk1.8 默認(rèn)開啟,其它版本未測(cè)試)
-XX:-EliminateLocks 關(guān)閉鎖消除
 
-XX:+EliminateAllocations 開啟標(biāo)量替換(jdk1.8 默認(rèn)開啟,其它版本未測(cè)試)
-XX:-EliminateAllocations 關(guān)閉就可以了
責(zé)任編輯:趙寧寧 來(lái)源: 寫代碼的SharkChili
相關(guān)推薦

2024-12-06 17:02:26

2019-01-18 12:39:45

云計(jì)算PaaS公有云

2018-09-14 17:16:22

云計(jì)算軟件計(jì)算機(jī)網(wǎng)絡(luò)

2024-10-05 00:00:06

HTTP請(qǐng)求處理容器

2024-09-18 08:10:06

2024-11-18 17:31:27

2020-07-02 15:32:23

Kubernetes容器架構(gòu)

2015-11-17 16:11:07

Code Review

2018-04-18 07:01:59

Docker容器虛擬機(jī)

2024-05-15 14:29:45

2010-05-26 17:35:08

配置Xcode SVN

2018-08-20 08:15:50

編程語(yǔ)言Go語(yǔ)言切片

2020-02-11 16:49:24

React前端代碼

2024-11-28 10:35:47

2024-04-10 07:48:41

搜索引擎場(chǎng)景

2015-10-15 14:16:24

2011-04-06 15:55:50

開發(fā)webOS程序webOS

2018-10-31 14:00:05

LispJavaScript編程語(yǔ)言

2024-11-13 15:18:51

JITWatch開發(fā)

2013-07-10 10:38:48

JavaScript框
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)