from:http://www.cnblogs.com/insistence/p/5901457.html
1. 什么是Just In Time編譯器?
Hot Spot 編譯
當 JVM 執(zhí)行代碼時,它并不立即開始編譯代碼。這主要有兩個原因:
首先,如果這段代碼本身在將來只會被執(zhí)行一次,那么從本質上看,編譯就是在浪費精力。因為將代碼翻譯成 java 字節(jié)碼相對于編譯這段代碼并執(zhí)行代碼來說,要快很多。
當 然,如果一段代碼頻繁的調用方法,或是一個循環(huán),也就是這段代碼被多次執(zhí)行,那么編譯就非常值得了。因此,編譯器具有的這種權衡能力會首先執(zhí)行解釋后的代 碼,然后再去分辨哪些方法會被頻繁調用來保證其本身的編譯。其實說簡單點,就是 JIT 在起作用,我們知道,對于 Java 代碼,剛開始都是被編譯器編譯成字節(jié)碼文件,然后字節(jié)碼文件會被交由 JVM 解釋執(zhí)行,所以可以說 Java 本身是一種半編譯半解釋執(zhí)行的語言。Hot Spot VM 采用了 JIT compile 技術,將運行頻率很高的字節(jié)碼直接編譯為機器指令執(zhí)行以提高性能,所以當字節(jié)碼被 JIT 編譯為機器碼的時候,要說它是編譯執(zhí)行的也可以。也就是說,運行時,部分代碼可能由 JIT 翻譯為目標機器指令(以 method 為翻譯單位,還會保存起來,第二次執(zhí)行就不用翻譯了)直接執(zhí)行。
第二個原因是最優(yōu)化,當 JVM 執(zhí)行某一方法或遍歷循環(huán)的次數(shù)越多,就會更加了解代碼結構,那么 JVM 在編譯代碼的時候就做出相應的優(yōu)化。
我 們將在后面講解這些優(yōu)化策略,這里,先舉一個簡單的例子:我們知道 equals() 這個方法存在于每一個 Java Object 中(因為是從 Object class 繼承而來)而且經(jīng)常被覆寫。當解釋器遇到 b = obj1.equals(obj2) 這樣一句代碼,它則會查詢 obj1 的類型從而得知到底運行哪一個 equals() 方法。而這個動態(tài)查詢的過程從某種程度上說是很耗時的。
在主流商用JVM(HotSpot、J9)中,Java程序一開始是通過解釋器(Interpreter)進行解釋執(zhí)行的。當JVM發(fā)現(xiàn)某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為“熱點代碼(Hot Spot Code)”,然后JVM會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優(yōu)化,完成這個任務的編譯器稱為:即時編譯器(Just In Time Compiler,JIT)
JIT編譯器是“動態(tài)編譯器”的一種,相對的“靜態(tài)編譯器”則是指的比如:C/C++的編譯器
JIT并不是JVM的必須部分,JVM規(guī)范并沒有規(guī)定JIT必須存在,更沒有限定和指導JIT。但是,JIT性能的好壞、代碼優(yōu)化程度的高低卻是衡量一款JVM是否優(yōu)秀的最關鍵指標之一,也是虛擬機中最核心且最能體現(xiàn)虛擬機技術水平的部分。
2. 編譯器與解釋器
首先,不是所有JVM都采用編譯器和解釋器并存的架構,但主流商用虛擬機,都同時包含這兩部分。
2.1 配合過程
當程序需要迅速啟動然后執(zhí)行的時候,解釋器可以首先發(fā)揮作用,編譯器不運行從而省去編譯時間,立即執(zhí)行程序
在程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的執(zhí)行效率
當程序運行環(huán)境中內存資源限制較大(如部分嵌入式系統(tǒng)中),可以使用解釋執(zhí)行來節(jié)約內存;反之,則可以使用編譯執(zhí)行來提升效率。
同時,解釋器還可以作為編譯器(C2才會激進優(yōu)化)激進優(yōu)化時的一個“逃生門”,讓編譯器根據(jù)概率選擇一些大多數(shù)時候都能提升運行速度的優(yōu)化手段,當激進優(yōu)化假設不成立。如:加載了新類后,類型繼承結構出現(xiàn)變化,出現(xiàn)“罕見陷阱(Uncommon Trap)”時,可以通過逆優(yōu)化(Deoptimization)退回到解釋狀態(tài)繼續(xù)執(zhí)行
(部分沒有解釋器的虛擬機,也會采用不進行激進優(yōu)化的C1編譯器擔任“逃生門”的角色)
2.2 解釋器 - Interpreter
Interpreter解釋執(zhí)行class文件,好像JavaScript執(zhí)行引擎一樣
特殊的例子:
- 最早的Sun Classic VM只有Interpreter
- BEA JRockit VM則只有Compiler,但它主要面向服務端應用,部署在其上的應用不重點關注啟動時間
2.3 編譯器 - Compiler
只說HotSpot JVM
1. C1和C2:
HotSpot虛擬機內置了兩個即時編譯器,分別稱為Client Compiler和Server Compiler,習慣上將前者稱為C1,后者稱為C2
2. 使用C1還是C2?
HotSpot默認采用解釋器和其中一個編譯器直接配合的方式工作,使用那個編譯器取決于虛擬機運行的模式,HotSpot會根據(jù)自身版本和宿主機器硬件性能自動選擇模式,用戶也可以使用“-client”或”-server”參數(shù)去指定
混合模式(Mixed Mode)
默認的模式,如上面描述的這種方式就是mixed mode解釋模式(Interpreted Mode)
可以使用參數(shù)“-Xint”,在此模式下全部代碼解釋執(zhí)行編譯模式(Compiled Mode)
參數(shù)“-Xcomp”,此模式優(yōu)先采用編譯,但是無法編譯時也會解釋(在最新的HotSpot中此參數(shù)被取消)可以看到,我的JVM現(xiàn)在是mixed mode
重要:↓
在JDK1.7(1.7僅包括Server模式)之后,HotSpot就不是默認“采用解釋器和其中一個編譯器”配合的方式了,而是采用了分層編譯,分層編譯時C1和C2有可能同時工作
3. 分層編譯
3.1 為什么要分層編譯?
由于編譯器compile本地代碼需要占用程序時間,要編譯出優(yōu)化程度更高的代碼所花費的時間可能更長,且此時解釋器還要替編譯器收集性能監(jiān)控信息,這對解釋執(zhí)行的速度也有影響
所以,為了在程序啟動響應時間與運行效率之間達到最佳平衡,HotSpot在JDK1.6中出現(xiàn)了分層編譯(Tiered Compilation)的概念并在JDK1.7的Server模式JVM中作為默認策略被開啟
3.2 編譯層 tier(或者叫級別)
分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時,劃分了不同的編譯層次(不只以下3種),包括:
第0層,程序解釋執(zhí)行(沒有編譯),解釋器不開啟性能監(jiān)控功能,可觸發(fā)第1層編譯。
第1層,也稱C1編譯,將字節(jié)碼編譯為本地代碼,進行簡單、可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯
第2層(或2層以上),也稱為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至會根據(jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化
實施分層編譯后,C1和C2將會同時工作,許多代碼會被多次編譯,用C1獲取更高的編譯速度,用C2來獲取更好的編譯質量,且在解釋執(zhí)行的時候解釋器也無須再承擔收集性能監(jiān)控信息的任務
4. 編譯對象與觸發(fā)條件
1. 誰被編譯了?
編譯對象就是之前說的“熱點代碼”,它有兩類:
- 被多次調用的方法
- 一個方法被多次調用,理應稱為熱點代碼,這種編譯也是虛擬機中標準的JIT編譯方式
- 被多次執(zhí)行的循環(huán)體
- 編譯動作由循環(huán)體出發(fā),但編譯對象依然會以整個方法為對象;
- 這種編譯方式由于編譯發(fā)生在方法執(zhí)行過程中,因此形象的稱為:棧上替換(On Stack Replacement- OSR編譯,即方法棧幀還在棧上,方法就被替換了)
2. 觸發(fā)條件
1. 綜述
上面的方法和循環(huán)體都說“多次”,那么多少算多?換個說法就是編譯的觸發(fā)條件。
判斷一段代碼是不是熱點代碼,是不是需要觸發(fā)JIT編譯,這樣的行為稱為:熱點探測(Hot Spot Detection),有幾種主流的探測方式:
基于計數(shù)器的熱點探測(Counter Based Hot Spot Detection)
虛擬機會為每個方法(或每個代碼塊)建立計數(shù)器,統(tǒng)計執(zhí)行次數(shù),如果超過閥值那么就是熱點代碼。缺點是維護計數(shù)器開銷。基于采樣的熱點探測(Sample Based Hot Spot Detection)
虛擬機會周期性檢查各個線程的棧頂,如果某個方法經(jīng)常出現(xiàn)在棧頂,那么就是熱點代碼。缺點是不精確。基于蹤跡的熱點探測(Trace Based Hot Spot Detection)
Dalvik中的JIT編譯器使用這種方式
2. HotSpot
HotSpot使用的是第1種,因此它為每個方法準備了兩類計數(shù)器:方法調用計數(shù)器(Invocation Counter)和回邊計數(shù)器(Back Edge Counter)
方法計數(shù)器
默認閥值,在Client模式下是1500次,Server是10000次,可以通過參數(shù)“-XX:CompileThreshold”來設定
當一個方法被調用時會首先檢查是否存在被JIT編譯過得版本,如果存在則使用此本地代碼來執(zhí)行;如果不存在,則將方法計數(shù)器+1,然后判斷“方法計數(shù)器和回邊計數(shù)器之和”是否超過閥值,如果是則會向編譯器提交一個方法編譯請求
默認情況下,執(zhí)行引擎并不會同步等待上面的編譯完成,而是會繼續(xù)解釋執(zhí)行。當編譯完成后,此方法的調用入口地址會被系統(tǒng)自動改寫為新的本地代碼地址
還有一點,熱度是會衰減的,也就是說不是僅僅+,也會-,熱度衰減動作是在虛擬機的GC執(zhí)行時順便進行的
回邊計數(shù)器
回邊,顧名思義,只有執(zhí)行到大括號”}”時才算+1
默認閥值,Client下13995,Server下10700
它的調用邏輯和方法計數(shù)器差不多,只不過遇到回邊指令時+1、超過閥值時會提交OSR編譯請求以及這里沒有熱度衰減
5. 編譯過程
編譯過程是在后臺線程(daemon)中完成的,可以通過參數(shù)“-XX:-BackgroundCompilation”來禁止后臺編譯,但此時執(zhí)行線程就會同步等待編譯完成才會執(zhí)行程序
- Client Compiler
C1編譯器是一個簡單快速的三段式編譯器,主要關注“局部性能優(yōu)化”,放棄許多耗時較長的全局優(yōu)化手段
過程:class -> 1. 高級中間代碼 -> 2. 低級中間代碼 -> 3. 機器代碼 - Server Compiler
C2是專門面向服務器應用的編譯器,是一個充分優(yōu)化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數(shù)時的優(yōu)化強度。
使用參數(shù)“-XX:+PrintCompilation”會讓虛擬機在JIT時把方法名稱打印出來,如圖:
6. Java和C/C++的編譯器對比
這里不是比Java和C/C++誰快這種大坑問題,只是比較編譯器(我認為開發(fā)效率上Java快,執(zhí)行效率上C/C++快)
這種對比代表了經(jīng)典的即時編譯器與靜態(tài)編譯期的對比,其實總體來說Java編譯器有優(yōu)有劣。主要就是動態(tài)編譯時間壓力大能做的優(yōu)化少,還要做一些動態(tài)校驗。而靜態(tài)編譯器無法實現(xiàn)一些開發(fā)上很有用的動態(tài)特性