java中的即時編譯(JIT)簡介


Java發展這么多年一直長青,很大一部分得益於開發人員長期對其堅持不懈的優化:寫得更少,跑得更快!JIT就是其中一項十分重要的優化。

JIT全程Java Intime Compiler,即Java即時編譯器。咦為啥Java的編譯器是一項優化呢?Java本來不就是編譯型語言嗎?聽我細細道來。

從我們最早接觸Java編程開始,學習到的就是手寫java文件,然后javac編譯、java運行主方法。

如果這里都看不懂可能不適合閱讀本文

javac會把.java文件編譯成.class文件,所以我們說Java是編譯型語言。當然Java是強類型的語言,通常我們說強類型的是編譯型的,弱類型的腳本語言(也叫動態語言,相對應的強類型語言叫靜態語言)。.class文件格式就是“字節碼”。編譯的過程見《Java文件的編譯》

為了實現“一次編寫,隨處運行”的目標,字節碼會被jvm運行。而這里的運行就是解釋執行,jvm是一行一行閱讀字節碼文件中的jvm指令,並把它翻譯成機器的cpu指令。這個過程就比較慢了(相對中低級語言而言)。

Java為了提高開發和運行效率,已經對語言和jvm在多方面做了優先,包括垃圾回收器、各種鎖機制,甚至最簡單的分支預測都大力優化。解釋執行的效率自然也被納入優化范圍。在1996年10月25號,當時的Java東家Sun發布了第一款JIT編譯器。那時還是java 2剛出來(Java1 和Java2差異較大,我們現在使用的jdk都是Java2上的迭代),離現在已經20多年了。目前JIT已經是默認開啟的,因為它帶來的效果明顯。除非通過參數指定不使用。

JIT的動機基於“二八定律”,20%的熱點代碼占據了程序80%的執行時間

即使開啟了JIT,也少不了代碼編譯和字節碼解釋的過程。JIT處理的是熱點代碼(hotspot code,或叫熱門代碼)。熱點代碼就是頻繁執行的代碼塊,比如循環里面的代碼。JIT有一套邏輯判斷是否熱點代碼。

既然JIT處理后的是機器能夠快速執行的代碼,為啥還要解釋執行呢,干嘛不把全部代碼編譯成機器代碼呢?這是由於編譯本地代碼比較費時間,而且編譯后還要進行進一步的優化導致耗時更久;而解釋器是能夠立即解釋字節碼文件的,畢竟我們的應用放到服務器上的時候就已經是字節碼文件了,解釋器可以拿來直接用。而且解釋器執行的時候占用的內存更小,在內存受限的場景難以使用編譯器(比如手機上)。編譯器會概率性地選擇多數時候都能提升運行效率的手段進行優化,如果“優化”后發現還不如不優化(甚至執行有問題)就得“逆優化”,回退到解釋執行狀態。

我們可以通過最簡單的查看Java版本的命令查看Java是否使用了編譯器:

 ~ > java -version

java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

最后輸出的“mixed mode”代表是混合模式,也就是先解釋執行,並逐步將熱點代碼代替為機器代碼。不使用編譯器的模式叫“interpreted mode”;優先使用編譯器的模式叫“compiled mode”,compiled mode會優先采用編譯方式執行程序,如果編譯執行有問題就回退到解釋執行。

谷歌的V8是沒有解釋器的(沒錯,就是那個執行JS的)。V8的原理可以參閱這個 Quora問答

理論上講,經過JIT的Java程序運行效率要高於C++。因為C++是靜態編譯,而JIT在運行中可以參考運行時數據。

HotSpot虛擬機中有兩個編譯器,一個是給客戶端用的叫client Compiler,另一個是服務器用的叫Server Compiler。一般的,把Client Compiler也叫C1編譯器,Server Compiler叫C2編譯器或Opto編譯器。虛擬機會根據自身版本與宿主機的硬件性能自動選擇運行模式,也可以使用 “-client”或“-server”參數去強制指定虛擬機運行在Client模式或Server模式。

熱點探測

熱點代碼有兩類:

  • 被多次執行的方法
  • 多次執行的循環體

怎么統計“多次”呢?虛擬機為每個代碼塊和方法設置了計數器,執行一次就加1。超過限定次數就認為是熱點代碼,開始JIT處理。給JIT去處理只是一個請求,並不會立即同步等待結果。因為JIT編譯比較耗時,在編譯完成前會繼續解釋執行。編譯器處理都是以方法為單位,所以第一類熱點代碼是標准的JIT編譯方式;對於第二種熱點代碼,JIT編譯器會處理包含該循環的方法。流程很簡單,細節很復雜。下圖來自極客學院:javac 編譯與 JIT 編譯

jit
考慮這個問題:方法在執行時會被放到棧上,對於計算密集型的方法,大量計算任務都在一個方法內循環。這滿足第二類熱點代碼,會被編譯。但是方法並沒有退出重新執行,編譯后的代碼怎么能夠執行呢?

這個對於早期的JIT的確是個問題,不過現在JVM用到了”棧上替換“的技術:在執行過程中如果編譯版本可用了,虛擬機會暫停,把編譯版本的方法替換到棧上。反之亦然,上面說過逆優化。

那到底是超過多少次?
HotSpot虛擬機有兩種計數器(方法會同時記錄這兩個計數),它們的閾值並不同。

  • 調用次數計數器,可以通過-XX:CompileThreadhold參數指定閾值,不指定默認C1是1500次,C2是1萬次。
  • 字節碼中向之前跳轉的指令叫“回邊”,回邊次數是回邊計數器。明顯這個針對的是第二類熱點代碼。它的閾值是算出來的,公式如下
OSR 閾值 = CompileThreshold * 
((OnStackReplacePercentage - InterpreterProfilePercentage)/100)

第一個參數CompileThreshold就是調用計數器,后面兩個也都可以通過-XX指定。默認InterpreterProfilePercentage是33,而OnStackReplacePercentage的默認值在客戶端和服務器模式不一樣,分別是933和140,所以閾值分別是13500和10700。

分層編譯(Tiered Compilation)

Tiered Compilation是Java7中出現的,目的是整合C1的快速編譯和C2的快速執行。因為C2使用了“激進”的優化手段,編譯較慢。Java7以前,一般要求快速啟動的GUI程序會選擇C1,偏好性能的服務器程序使用C2。

Tiered Compilation將編譯分為0到4五級,怎么區分呢?看圖吧,我並不太懂(出處見水印,侵刪):

Tiered Compilation
好吧其實圖中並沒他們的區別,只是有無profiling而已。

java 10中引入了編譯更慢的Graal代替C2成為了第五級編譯器。C2是用C++編寫的,Graal是Java編寫的。只是兩種語言而已,為什么要用Java重寫一個編譯器呢?

因為C2中的全部優化能力已經全部移植到了Graal上,而Graal上面有一些算法(比如inlining算法及partial escape analysis)並不能用C++實現。

Inlining被稱為優化之母,因為它能引發更深的優化,能將對getter、setter的訪問優化成單一內存訪問。

常見的逃逸分析針對的就是鎖去除。如果對象被單一線程訪問,則可去除鎖;如果對象是堆分配且僅被單一方法訪問,則可轉化成棧分配,並伴隨將對字段的訪問替換成對操作數的訪問,從而進一步將棧分配轉換成虛擬分配。另外一大逃逸分析場景是for-loop。

Java10默認激進優化器依然是C2,要使用Graal需要使用參數-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler來開啟。

參考文獻:

動態編譯與性能測量

Java 即時編譯器JIT機制以及編譯優化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM