JAVA簡介
- 基本語言特性(面向?qū)ο螅ǚ庋b,繼承,多態(tài)),泛型,Lambda,反射)
- 平臺無關性(JVM運行.class文件,符合平臺的字節(jié)碼)
- 核心類庫(集合,并發(fā),網(wǎng)絡,IO/NIO,安全類庫等)
- JDK (Java開發(fā)工具包,JRE,JVM,API類庫)
- JRE(Java運行環(huán)境,JVM,Javase核心類庫)
- JVM(垃圾收集器,運行時,動態(tài)編譯,輔助功能JFR等)
- JVM作為一個平臺,不僅僅Java語言可以運行在JVM上,本質(zhì)上合規(guī)的字節(jié)碼都可以運行,比如:Clojure、Scala、Groovy、JRuby、Jython等大量JVM語言
JAVA如何運行的?
- Java源碼經(jīng)過Javac編譯成.class文件
- .class文件經(jīng)過JVM解析或編譯運行
- 解釋器解析:.class文件經(jīng)過JVM內(nèi)嵌的解析器解析執(zhí)行;
- 即時編譯器JIT:把經(jīng)常運行的 字節(jié)碼 作為“熱點代碼”編譯成與本地平臺相關的機器碼,并進行各層次的優(yōu)化;
- 預編譯器AOT:JDK9引入這個特性,并且增加了新的jaotc工具,
將代碼編譯成機器碼執(zhí)行。
JVM運行期優(yōu)化

為何HotSpot虛擬機要使用解釋器與編譯器并存架構(gòu)?
解釋器優(yōu)點:
- 當程序需要快速啟動和執(zhí)行的時候,省去了編譯的時間,立即執(zhí)行;
- 作為編譯器激進優(yōu)化的“逃生門”,當激進優(yōu)化的假設不成立的時候(比如:加載新類后類型繼承結(jié)構(gòu)出現(xiàn)變化、出現(xiàn)”罕見陷阱(Uncommon Trap)” 等時候,通過逆向優(yōu)化(Deoptimization)退回到解釋狀態(tài)繼續(xù)執(zhí)行,(部分沒有解釋器的虛擬機中也會采用不進行激進優(yōu)化的C1編譯器擔任“逃生門”的角色);
- 當程序運行環(huán)境資源限制較大(如嵌入式系統(tǒng)),可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率。
編譯器優(yōu)點:
- 在程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的執(zhí)行效率;
- 基于運行分析,進行熱點代碼編譯的設計,是因為絕大部分程序都表現(xiàn)為“小部分的熱點代碼耗費了大多數(shù)的資源”。
為何HotSpot虛擬機要使用兩個不同的即時編譯器?
由于即時編譯器編譯字節(jié)碼需要占用程序運行時間,要編譯出優(yōu)化程度更高的代碼所花費的時間可能更長;而且想要編譯出優(yōu)化程度跟高的代碼,解釋器可能還要替代編譯器收集性能監(jiān)控信息,這對解釋器執(zhí)行的速度也有影響;為了在程序啟動響應速度與運行效率之間達到最佳平衡,HotSpot虛擬機還會逐漸啟用分層編譯(Tiered Compilation)的策略。
兩個編譯器分別稱為:C1(Client Compile)和 C2(Server Compile);
- 分成編譯(Tiered Compilation):
- 第 0 層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能(Profiling) 可觸發(fā)第 1 層編譯;
- 第 1 層:也稱為C1編譯,將字節(jié)碼編譯為本地代碼,進行簡單、可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯;
- 第 2 層或及以上:也成為C2編譯,也將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時的優(yōu)化,甚至會根據(jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化。
- 實施分成編譯后,C1 和 C2 將會同時工作,許多字節(jié)碼都會被多次編譯,用 C1 獲取更高的編譯速度, 用 C2 來獲取更好的編譯質(zhì)量,在解釋執(zhí)行的時候也無須再承擔收集性能監(jiān)控信息的任務。
程序何時使用解釋器執(zhí)行?何時使用編譯器執(zhí)行?哪些代碼會編譯為本地代碼
-
即時編譯的“熱點代碼”有兩類,即:(方法級粒度,以整個方法作為編譯對象)
- 被多次調(diào)用的方法;
- 被多次執(zhí)行的循環(huán)體(方法內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體)。
前者很好理解,一個方法被調(diào)用得多了,方法體內(nèi)代碼執(zhí)行的次數(shù)自然就多,它成為“熱點代碼”是理所當然的。而后者則是為了解決一個方法只被調(diào)用過一次或少量的幾次,但是方法體內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體的問題,這樣循環(huán)體的代碼也被重復執(zhí)行多次,因此這些代碼也應該認為是“熱點代碼”;
對于第一種情況,由于是由方法調(diào)用觸發(fā)的編譯,因此編譯器理所當然地會以整個方法作為編譯對象,這種編譯也是虛擬機中標準的JIT編譯方式。而對于后一種情況,盡管編譯動作是由循環(huán)體所觸發(fā)的,但編譯器依然會以整個方法(而不是單獨的循環(huán)體)作為編譯對象。這種編譯方式因為編譯發(fā)生在方法執(zhí)行過程之中,因此形象地稱之為棧上替換(On Stack Replacement,簡稱為OSR編譯,即方法棧幀還在棧上,方法就被替換了)。
-
判斷一段代碼是不是 “ 熱點代碼 ”,是不是需要觸發(fā)即時編譯,這樣的行為稱為 熱點探測,目前主要的熱點探測判斷方式有兩種,分別如下:
-
基于采樣的熱點探測(Sample Based Hot Spot Detection): 采用這種方法的虛擬機會周期性地檢查各個線程的棧頂, 如果發(fā)現(xiàn)某個(或某些)方法經(jīng)常出現(xiàn)在棧頂,那這個方法就是“熱點代碼”;
- 優(yōu)點:實現(xiàn)簡單、高效,還可以很容易地獲取方法調(diào)用關系(將調(diào)用堆棧展開即可);
- 缺點:很難精確地確認一個方法的熱度,容易因為受線程阻塞或別的外界因素的影響而擾亂熱點探測。
-
基于計數(shù)器的熱點探測(Counter Based Hot Spot Detection): 采用這種方法的虛擬機會為每個方法(甚至是代碼塊)建立計數(shù)器,統(tǒng)計方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就認為它是“熱點代碼”;
- 優(yōu)點:結(jié)果更加精確和嚴謹;
- 缺點:統(tǒng)計方法實現(xiàn)起來麻煩些,需要建立并維護計數(shù)器,而且不能直接獲取方法的調(diào)用關系。
-
-
HotSpot虛擬機使用的是第2種-基于計數(shù)器的熱點探測方法,因此它為每個方法準備了兩類計數(shù)器:[方法調(diào)用計數(shù)器(Invocation Counter) ] 和 [回邊計數(shù)器(Back Edge Counter)]:
- 方法調(diào)用計數(shù)器(Invocation Counter):
閾值(默認是:C1-1500, C2-10000),可以通過虛擬機參數(shù)[-XX:CpmpileThreshold] 來人為設置;
當一個方法被調(diào)用時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行。如果不存在已被編譯過的版本,則將此方法的調(diào)用計數(shù)器值加1,然后判斷方法調(diào)用計數(shù)器與回邊計數(shù)器值之和是否超過方法調(diào)用計數(shù)器的閾值。如果已超過閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求;
如果不做任何設置,執(zhí)行引擎并不會同步等待編譯請求完成,而是繼續(xù)進入解釋器按照解釋方式執(zhí)行字節(jié)碼,這個地方是異步形式,直到提交請求被編譯器編譯完成,當編譯工作完成之后,這個方法調(diào)用入口地址就會被系統(tǒng)自動改寫成新的,下一次調(diào)用該方法時,就會使用已編譯的版本;
如果不做任何設置,方法計數(shù)器統(tǒng)計的并不是方法調(diào)用的絕對次數(shù),而是一個相對次數(shù),即一段時間內(nèi)方法被調(diào)用的次數(shù);當超過一定時間限度,如果方法的調(diào)用次數(shù)仍然不足讓它提交給即時編譯器編譯,那這個方法的調(diào)用計數(shù)器就會被減半,這個過程稱為[方法調(diào)用計數(shù)器的衰減(Counter Decay)],進行熱度衰減的動作是在虛擬機進行垃圾收集時順便進行的,可以是用虛擬機參數(shù)[-XX: -UserCounterDecay]來關閉熱度衰減,同時也可以使用虛擬機參數(shù)[***-XX: CounterHalfLifeTime]設置半衰周期的時間,單位秒。
-
JIT編譯交互過程如下:
Invocation Counter JIT Interaction flow
- 方法調(diào)用計數(shù)器(Invocation Counter):
- 回邊計數(shù)器(Back Edge Counter):(目的就是為了觸發(fā)OSR編譯)
-
虛擬機運行在Clinent(C1)模式下,OnStackReplacePercentage默認值為933,都是取默認值的情況下,Client模式下回邊計數(shù)器的閾值是13995,回邊計數(shù)器閾值計算公式為:
方法調(diào)用計數(shù)器閾值(CompileThreshold)X OSR比率(OnStackReplacePercentage)/ 100 -
虛擬機運行在Server(C2)模式下, OnStackReplacePercentage默認值為140,InterpreterProfilePercentage默認值為33,都是取默認值的情況下,Server模式下回邊計數(shù)器的閾值是10700,回邊計數(shù)器閾值計算公式為:
方法調(diào)用計數(shù)器閾值(CompileThreshold)X(OSR比率(OnStackReplacePercentage)- 解釋器監(jiān)控比率(InterpreterProfilePercentage))/ 100 當解釋器遇到一條回邊指令時,會先查找將要執(zhí)行的代碼片段是否有已經(jīng)編譯好的版本,如果有,它將會優(yōu)先執(zhí)行已編譯的代碼,否則就把回邊計數(shù)器的值加1,然后判斷方法調(diào)用計數(shù)器與回邊計數(shù)器值之和是否超過回邊計數(shù)器的閾值。當超過閾值的時候,將會提交一個OSR編譯請求,并且把回邊計數(shù)器的值降低一些,以便繼續(xù)在解釋器中執(zhí)行循環(huán),等待編譯器輸出編譯結(jié)果。
JIT編譯交互過程如下:
-

- 總結(jié)
- 相比于方法計數(shù)器,回邊計數(shù)器沒有計數(shù)熱度衰減過程,因此這個計數(shù)器統(tǒng)計的就是該方法循環(huán)體執(zhí)行的絕對次數(shù);
- 以上兩點僅僅描述了Client VM(C1)的即時編譯方式。
如何編譯為本地代碼
- 在默認設置下,無論是方法調(diào)用產(chǎn)生的即時編譯請求,還是OSR編譯請求,虛擬機在代碼編譯器還未完成之前,都仍然將按照解釋方式繼續(xù)執(zhí)行,而編譯動作則在后臺的編譯線程中進行。用戶可以通過參數(shù)-XX:-BackgroundCompilation來禁止后臺編譯,在禁止后臺編譯后,一旦達到JIT的編譯條件,執(zhí)行線程向虛擬機提交編譯請求后將會一直等待,直到編譯過程完成后再開始執(zhí)行編譯器輸出的本地代碼。
-
對于Client Compiler來說,它是一個簡單快速的三段式編譯器,主要的關注點在于局部性的優(yōu)化,而放棄了許多耗時較長的全局優(yōu)化手段:
在第一個階段,一個平臺獨立的前端將字節(jié)碼構(gòu)造成一種高級中間代碼表示(High-Level Intermediate Representaion,HIR)。HIR使用靜態(tài)單分配(Static Single Assignment,SSA)的形式來代表代碼值,這可以使得一些在HIR的構(gòu)造過程之中和之后進行的優(yōu)化動作更容易實現(xiàn)。在此之前編譯器會在字節(jié)碼上完成一部分基礎優(yōu)化,如方法內(nèi)聯(lián)、常量傳播等優(yōu)化將會在字節(jié)碼被構(gòu)造成HIR之前完成。
在第二個階段,一個平臺相關的后端從HIR中產(chǎn)生低級中間代碼表示(Low-Level Intermediate Representation,LIR),而在此之前會在HIR上完成另外一些優(yōu)化,如空值檢查消除、范圍檢查消除等,以便讓HIR達到更高效的代碼表示形式。
最后階段是在平臺相關的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窺孔(Peephole)優(yōu)化,然后產(chǎn)生機器代碼。
Server Compiler則是專門面向服務端的典型應用并為服務端的性能配置特別調(diào)整過的編譯器,也是一個充分優(yōu)化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數(shù)時的優(yōu)化強度,它會執(zhí)行所有經(jīng)典的優(yōu)化動作,如無用代碼消除(Dead Code Elimination)、循環(huán)展開(Loop Unrolling)、循環(huán)表達式外提(Loop Expression Hoisting)、消除公共子表達式(Common Subexpression Elimination)、常量傳播(Constant Propagation)、基本塊重排序(Basic Block Reordering)等,還會實施一些與Java語言特性密切相關的優(yōu)化技術(shù),如范圍檢查消除(Range Check Elimination)、空值檢查消除(Null Check Elimination,不過并非所有的空值檢查消除都是依賴編譯器優(yōu)化的,有一些是在代碼運行過程中自動優(yōu)化了)等。另外,還可能根據(jù)解釋器或Client Compiler提供的性能監(jiān)控信息,進行一些不穩(wěn)定的激進優(yōu)化,如守護內(nèi)聯(lián)(Guarded Inlining)、分支頻率預測(Branch Frequency Prediction)等;
Server Compiler的寄存器分配器是一個全局圖著色分配器,它可以充分利用某些處理器架構(gòu)(如RISC)上的大寄存器集合。以即時編譯的標準來看,Server Compiler無疑是比較緩慢的,但它的編譯速度依然遠遠超過傳統(tǒng)的靜態(tài)優(yōu)化編譯器,而且它相對于Client Compiler編譯輸出的代碼質(zhì)量有所提高,可以減少本地代碼的執(zhí)行時間,從而抵消了額外的編譯時間開銷,所以也有很多非服務端的應用選擇使用Server模式的虛擬機運行。

文中如有錯誤點請指出,互相學習,謝謝!
參考資料:
- 《深入理解Java虛擬機》周志明著
- http://www.itdecent.cn/p/bb00e6a00280
- http://www.itdecent.cn/p/a1a7e49a8a61
