天天看點

淺談Java JIT編譯器概念

一、解釋器

淺談Java JIT編譯器概念

Java程式在運作的時候,主要就是執行位元組碼指令,一般這些指令會按照順序解釋執行,這種就是解釋執行。

淺談Java JIT編譯器概念

但是那些被頻繁調用的代碼,比如調用次數很高或者在 for 循環裡的那些代碼,如果按照解釋執行,效率是非常低的。

以上的這些代碼稱為熱點代碼。是以,為了提高熱點代碼的執行效率,在運作時,虛拟機将會把這些代碼編譯成與本地平台相關的機器碼,并進行各種層次的優化。

二、編譯器

完成這個任務的編譯器,就稱為即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器。

淺談Java JIT編譯器概念

JVM 內建的編譯器有兩種模式:

◉ Client Compiler

◉ Server Compiler

Client Compiler

  • 注重新開機動速度和局部優化,HotSpot VM 使用的是 Client Compiler C1編譯器,簡稱 C1編譯器。

Server Compiler

  • 注重全局優化,運作過程中性能更好,由于進行更多的全局分析,是以啟動速度會變慢。Hotspot VM 使用有兩種:C2編譯器(預設)和 Graal編譯器(暫時不讨論)。

1、傳統編譯器

在JDK1.8中 HotSpot 虛拟機中,内置了兩個 JIT,分别為 C1 編譯器和 C2 編譯器。

1)C1編譯器

C1 編譯器是一個簡單快速的編譯器,主要的關注點在于局部性的優化,适用于執行時間較短或對啟動性能有要求的程式,例如,GUI 應用對界面啟動速度就有一定要求,C1也被稱為 Client Compiler。

C1編譯器幾乎不會對代碼進行優化

2)C2編譯器

C2 編譯器是為長期運作的伺服器端應用程式做性能調優的編譯器,适用于執行時間較長或對峰值性能有要求的程式。根據各自的适配性,這種即時編譯也被稱為Server Compiler。

但是C2代碼已超級複雜,無人能維護!是以才會開發Java編寫的Graal編譯器取代C2(JDK10開始)

3)分層編譯

C1、C2 都有各自的優缺點,為了綜合兩者的優勢,在編譯速度和執行效率之間取得平衡,JVM 引入了一種政策:分層編譯。

◉ 0級(解釋執行):采用解釋執行,不開啟性能監控功能(profiling)。
◉ 1級(簡單的 C1 編譯):采用 C1編譯器,進行簡單可靠的優化,不開啟性能監控功能。
◉ 2級(有限的 C1 編譯):采用 C1編譯器,進行更多的優化編譯,開啟性能監控功能,統計方法計數器的值,以及回邊計數器的值等。
◉ 3級(完全 C1 編譯):完全使用 C1編譯器 的所有功能,完全開啟性能監控功能。
◉ 4級(C2 編譯):完全使用 C2編譯器 進行編譯,進行完全的優化。
注意:分層編譯隻能在 Server Compiler 模式下啟用。

其中,這幾個層次的執行效率是這樣的:4 > 1 > 2 > 3 > 0。

C2 代碼的執行效率比 C1 的高出30%以上。

1 > 2 > 3 的主要原因是性能監控功能開啟越多,其性能開銷也越大。

列舉幾種常見的編譯路徑,如下圖所示:

淺談Java JIT編譯器概念

在 Java7之前,需要根據程式的特性來選擇對應的 JIT,虛拟機預設采用解釋器和其中一個編譯器配合工作。

Java7及以後引入了分層編譯,這種方式綜合了 C1 的啟動性能優勢和 C2 的峰值性能優勢,當然我們也可以通過參數強制指定虛拟機的即時編譯模式。

在 Java8 中,預設開啟分層編譯

  • 1、通過java -version 指令行可以直接檢視到目前系統使用的編譯模式(預設分層編譯)
    淺談Java JIT編譯器概念
  • 2、使用-Xint參數強制虛拟機運作于隻有解釋器的編譯模式
    淺談Java JIT編譯器概念
  • 3、使用-Xcomp強制虛拟機運作于隻有 JIT的編譯模式下
    淺談Java JIT編譯器概念

2、GraalVM

GraalVM 是一個高性能 JDK 發行版,旨在加速用Java和其他JVM語言編寫的應用程式的執行,并支援 JavaScript、Ruby、Python 和許多其他流行語言。

淺談Java JIT編譯器概念

Graal Compiler是GraalVM與HotSpotVM(從JDK10起)共同擁有的服務端即時編譯器,是C2編譯器的替代者。

1)Graal 和 C2 的差別

Graal 和 C2 最為明顯的一個差別是:Graal 是用 Java 寫的,而 C2 是用 C++ 寫的。相對來說,Graal 更加子產品化,也更容易開發與維護,畢竟,連C2的開發者都不想去維護C2了。

許多人會覺得用 C++ 寫的 C2 肯定要比 Graal 快。實際上,在充分預熱的情況下,Java 程式中的熱點代碼早已經通過即時編譯轉換為二進制碼,在執行速度上并不亞于靜态編譯的 C++ 程式。

Graal 的内聯算法對新文法、新語言更加友好,例如 Java 8 的 lambda 表達式以及 Scala 語言。

2)JVMCI

前文解釋過,編譯器是 Java 虛拟機中相對獨立的子產品,它主要負責接收 Java 位元組碼,并生成可以直接運作的二進制碼。

傳統情況下(JDK8),即時編譯器是與 Java 虛拟機緊耦合的。也就是說,對即時編譯器的更改需要重新編譯整個 Java 虛拟機。這對于開發相對活躍的 Graal 來說顯然是不可接受的。

為了讓 Java 虛拟機與 Graal 解耦合,我們引入了Java 虛拟機編譯器接口(JVM Compiler Interface,JVMCI),将即時編譯器的功能抽象成一個 Java 層面的接口。這樣一來,在 Graal 所依賴的 JVMCI 版本不變的情況下,我們僅需要替換 Graal 編譯器相關的 jar 包(Java 9 以後的 jmod 檔案),便可完成對 Graal 的更新

3)AOT

Ahead-of-time compile(aot,提前編譯),他在編譯期時,會把所有相關的東西,包含一個基底的 VM,一起編譯成機器碼(二進制)。

graal 的 aot 屬于“GraalVM ”中的一項技術,好處是可以更快速的啟動一個 java 應用(以往如果要啟動 java程式,需要先啟動 jvm 再載入 java 代碼,然後再即時的将 .class 位元組碼編譯成機器碼,交給機器執行,非常耗時間和耗記憶體,而如果使用AOT,可以取得一個更小更快速的鏡像,适合用在雲部署上)

4)特點

GraalVM是一款高性能的可嵌入式多語言虛拟機,它能運作不同的程式設計語言

  • 基于JVM的語言,比如Java, Scala, Kotlin和Groovy
  • 解釋型語言,比如JavaScript, Ruby, R和Python
  • 配合LLVM一起工作的原生語言,比如C, C++, Rust和Swift

GraalVM的設計目标是可以在不同的環境中運作程式

  • 在JVM中
  • 編譯成獨立的本地鏡像(不需要JDK環境)
  • 将Java及本地代碼子產品內建為更大型的應用

三、熱點代碼

熱點代碼,就是那些被頻繁調用的代碼,比如調用次數很高或者在 for 循環裡的那些代碼。這些再次編譯後的機器碼會被緩存起來,以備下次使用,但對于那些執行次數很少的代碼來說,這種編譯動作就純屬浪費。

JVM提供了一個參數“-XX:ReservedCodeCacheSize”,用來限制 CodeCache 的大小。也就是說,JIT 編譯後的代碼都會放在 CodeCache 裡,預設大小240M。

如果這個空間不足,JIT 就無法繼續編譯,編譯執行會變成解釋執行,性能會降低一個數量級。同時,JIT 編譯器會一直嘗試去優化代碼,進而造成了 CPU 占用上升。

通過 java -XX:+PrintFlagsFinal –version查詢:

淺談Java JIT編譯器概念

1)熱點探測

在 HotSpot 虛拟機中的熱點探測是 JIT 優化的條件,熱點探測是基于計數器的熱點探測,采用這種方法的虛拟機會為每個方法建立計數器統計方法的執行次數,如果執行次數超過一定的門檻值就認為它是“熱點方法”

虛拟機為每個方法準備了兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)。在确定虛拟機運作參數的前提下,這兩個計數器都有一個确定的門檻值,當計數器超過門檻值溢出了,就會觸發 JIT 編譯。

2)方法調用計數器

用于統計方法被調用的次數,方法調用計數器的預設門檻值在用戶端模式下是 1500 次,在服務端模式下是 10000 次(我們用的都是服務端,java –version查詢),可通過 -XX: CompileThreshold 來設定

淺談Java JIT編譯器概念

通過 java -XX:+PrintFlagsFinal –version查詢:

淺談Java JIT編譯器概念

四、編譯器優化技術

JIT 編譯運用了一些經典的編譯優化技術來實作代碼的優化,即通過一些例行檢查優化,可以智能地編譯出運作時的最優性能代碼.

1、方法内聯

方法内聯的優化行為就是把目标方法的代碼複制到發起調用的方法之中,避免發生真實的方法調用。

例如以下方法:

淺談Java JIT編譯器概念

最終會被優化為:

淺談Java JIT編譯器概念

JVM 會自動識别熱點方法,并對它們使用方法内聯進行優化。

我們可以通過 -XX:CompileThreshold 來設定熱點方法的門檻值。

但要強調一點,熱點方法不一定會被 JVM 做内聯優化,如果這個方法體太大了,JVM 将不執行内聯操作。

而方法體的大小門檻值,我們也可以通過參數設定來優化:

經常執行的方法,預設情況下,方法體大小小于 325 位元組的都會進行内聯,我們可以通過 -XX:FreqInlineSize=N 來設定大小值;

淺談Java JIT編譯器概念

不是經常執行的方法,預設情況下,方法大小小于 35 位元組才會進行内聯,我們也可以通過 -XX:MaxInlineSize=N 來重置大小值。

淺談Java JIT編譯器概念
即方法大小滿足:FreqInlineSize(預設325位元組) > 方法大小 > MaxInlineSize(預設35位元組),時才會觸發觸發方法内聯優化

熱點方法的優化可以有效提高系統性能,一般我們可以通過以下幾種方式來提高方法内聯:

  • 通過設定 JVM 參數來減小熱點門檻值或增加方法體門檻值,以便更多的方法可以進行内聯,但這種方法意味着需要占用更多地記憶體;
  • 在程式設計中,避免在一個方法中寫大量代碼,習慣使用小方法體;
  • 盡量使用 final、private、static 關鍵字修飾方法,編碼方法因為繼承,會需要額外的類型檢查。

2、鎖消除

在非線程安全的情況下,盡量不要使用線程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 關鍵字修飾,會使用到鎖,進而導緻性能下降。

淺談Java JIT編譯器概念

但實際上,在以下代碼測試中,StringBuffer 和 StringBuilder 的性能基本沒什麼差別。這是因為在局部方法中建立的對象隻能被目前線程通路,無法被其它線程通路,這個變量的讀寫肯定不會有競争,這個時候 JIT 編譯會對這個對象的方法鎖進行鎖消除。

在下面的測試代碼中,StringBuffer 和 StringBuilder 的性能基本沒什麼差別。這是因為在局部方法中建立的對象隻能被目前線程通路,無法被其它線程通路,這個變量的讀寫肯定不會有競争,這個時候 JIT 編譯會對這個對象的方法鎖進行鎖消除。

淺談Java JIT編譯器概念
淺談Java JIT編譯器概念

把鎖消除關閉—測試發現性能差别有點大

-XX:+EliminateLocks開啟鎖消除(jdk1.8預設開啟,其它版本未測試)

-XX:-EliminateLocks 關閉鎖消除

淺談Java JIT編譯器概念

3、标量替換

逃逸分析證明一個對象不會被外部通路,如果這個對象可以被拆分的話,當程式真正執行的時候可能不建立這個對象,而直接建立它的成員變量來代替。将對象拆分後,可以配置設定對象的成員變量在棧或寄存器上,原本的對象就無需配置設定記憶體空間了。這種編譯優化就叫做标量替換(前提是需要開啟逃逸分析)。

  • -XX:+DoEscapeAnalysis:開啟逃逸分析(jdk1.8預設開啟)
  • -XX:-DoEscapeAnalysis:關閉逃逸分析
  • -XX:+EliminateAllocations:開啟标量替換(jdk1.8預設開啟)
  • -XX:-EliminateAllocations:關閉标量替換

4、逃逸分析

逃逸分析的原理:分析對象動态作用域,當一個對象在方法中定義後,它可能被外部方法所引用。

比如:調用參數傳遞到其他方法中,這種稱之為方法逃逸。甚至還有可能被外部線程通路到,例如:指派給其他線程中通路的變量,這個稱之為線程逃逸。

從不逃逸到方法逃逸到線程逃逸,稱之為對象由低到高的不同逃逸程度。

如果确定一個對象不會逃逸出線程之外,那麼讓對象在棧上配置設定記憶體可以提高JVM的效率。

當然逃逸分析技術屬于JIT的優化技術,是以必須要符合熱點代碼,JIT才會優化,另外對象如果要配置設定到棧上,需要将對象拆分,這種編譯優化就叫做标量替換技術。

如果是逃逸分析出來的對象可以在棧上配置設定的話,那麼該對象的生命周期就跟随線程了,就不需要垃圾回收,如果是頻繁的調用此方法則可以得到很大的性能提高。

采用了逃逸分析後,滿足逃逸的對象在棧上配置設定

沒有開啟逃逸分析,對象都在堆上配置設定,會頻繁觸發垃圾回收(垃圾回收會影響系統性能),導緻代碼運作慢

最終可總結為下圖:

  • 1、要建立一個對象的時候,會判斷該對象是否為熱點代碼
  • 2、如果不是直接建立到堆中(不考慮對象過大,以及後續堆中的動态年齡判定、大小判定等情況)
  • 3、如果時熱點代碼(服務端執行次數超過設定的10000次),則判定是否開啟逃逸分析以及能否逃逸
  • 4、如果沒有開啟逃逸分析或者是不滿足逃逸分析的條件,則同第二步的執行流程
  • 5、初次之外的其他情況,則繼續判定是否開啟了标量替換
  • 6、如果沒有開啟标量替換,則同第二步的執行流程
  • 7、如果開啟了标量替換,則将該對象配置設定在堆中