執行引擎屬於JVM的下層,裏面包括 直譯器、及時編譯器、垃圾回收器
執行引擎是Java虛擬機器核心的組成部分之一。
「虛擬機器」是一個相對於「物理機」的概念,這兩種機器都有程式碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、快取、指令集和操作系統層面上的,而虛擬機器的執行引擎則是由軟體自行實現的,因此可以不受物理條件制約
地定製指令集與執行引擎的結構體系,能夠執行那些不被硬體直接支援的指令集格式。
JVM的主要任務是負責裝載位元組碼到其內部,但位元組碼並不能夠直接執行在操作系統之上,因爲位元組碼指令並非等價於本地機器指令,它內部包含的僅僅只是一些能夠被JVM所識別的位元組碼指令、符號表,以及其他輔助資訊。
那麼,如果想要讓一個Java程式執行起來,執行引擎(Execution Engine
)的任務就是將位元組碼指令解釋 / 編譯爲對應平臺上的本地機器指令纔可以。
這個與將原始碼編譯成位元組碼的編譯不同,那個稱爲前端編譯;而執行引擎的編譯稱爲後端編譯。
簡單來說,JVM中的執行引擎充當了將高階語言翻譯爲機器語言的譯者。
從外觀上來看,所有的Java虛擬機器的執行引擎輸入,輸出都是一致的:
大部分的程式程式碼轉換成物理機的目的碼或虛擬機器能執行的指令集之前,都需要經過下圖中的各個步驟
Java程式碼編譯是由Java原始碼編譯器來完成,流程圖如下所示:
Java位元組碼的執行是由JVM執行引擎來完成,流程圖 如下所示
我們用一個例子來說說 直譯器和編譯器:將人類的各種語言翻譯成機器能識別的指令
什麼是直譯器(Interpreter)
當Java虛擬機器啓動時會根據預定義的規範對位元組碼採用逐行解釋的方式執行,將每條位元組碼檔案中的內容「翻譯」爲對應平臺的本地機器指令執行。
什麼是 JIT編譯器
JIT(Just In Time Compiler)編譯器:就是虛擬機器將原始碼直接編譯成和本地機器平臺相關的機器語言。
爲什麼說Java是半編譯半直譯語言?
JDK1.0 時代,將 Java 語言定位爲「解釋執行」還是比較準確的。再後來,Java也發展出可以直接生成原生代碼的編譯器。
現在JVM在執行Java程式碼的時候,通常都會=將解釋執行與編譯執行二者結合起來進行。
各種用二進制編碼方式表示的指令,叫做機器指令碼。開始,人們就用它採編寫程式,這就是機器語言。
機器語言雖然能夠被計算機理解和接受,但和人們的語言差別太大,不易被人們理解和記憶,並且用它程式設計容易出差錯。
用它編寫的程式一經輸入計算機,CPU直接讀取執行,因此和其他語言編的程式相比,執行速度最快。
機器指令與CPU緊密相關,所以不同種類的CPU所對應的機器指令也就不同。
由於機器碼是有0和1組成的二進制序列,可讀性實在太差,於是人們發明了指令。
指令就是把機器碼中特定的0和1序列,簡化成對應的指令(一般爲英文簡寫,如mov,inc等),可讀性稍好
由於不同的硬體平臺,執行同一個操作,對應的機器碼可能不同,所以不同的硬體平臺的同一種指令(比如mov),對應的機器碼也可能不同。
不同的硬體平臺,各自支援的指令,是有差別的。因此每個平臺所支援的指令,稱之爲對應平臺的指令集。 如常見的
由於指令的可讀性還是太差,於是人們又發明了彙編語言。
在彙編語言中,用助記符(Mnemonics)代替機器指令的操作碼,用地址符號(Symbo1)或標號(Labe1)代替指令或運算元的地址。
在不同的硬體平臺,彙編語言對應着不同的機器語言指令集,通過彙編過程轉換成機器指令。
由於計算機只認識指令碼,所以用彙編語言編寫的程式還必須翻譯成機器指令碼,計算機才能 纔能識別和執行。
爲了使計算機使用者程式設計序更容易些,後來就出現了各種高階計算機語言。
高階語言比機器語言、彙編語言更接近人的語言;當計算機執行高階語言編寫的程式時,仍然需要把程式解釋和編譯成機器的指令碼。完成這個過程的程式就叫做解釋程式或編譯程式。
編譯過程又可以分成兩個階段:編譯和彙編。
編譯過程:是讀取源程式(字元流),對之進行詞法和語法的分析,將高階語言指令轉換爲功能等效的彙編程式碼
彙編過程:實際上指把彙編語言程式碼翻譯成目標機器指令的過程。
位元組碼是一種中間狀態(中間碼)的二進制程式碼(檔案),它比機器碼更抽象,需要直譯器轉譯後才能 纔能成爲機器碼
位元組碼主要爲了實現特定軟體執行和軟體環境、與硬體環境無關。
位元組碼的實現方式是通過編譯器和虛擬機器器。
Java bytecode
爲什麼不直接將原始碼翻譯成彙編,再翻譯成機器指令執行,而是翻譯成位元組碼檔案後再去翻譯成彙編和機器指令?
JVM設計者們的初衷僅僅只是單純地爲了滿足Java程式實現跨平臺特性,因此避免採用靜態編譯的方式直接生成本地機器指令,從而誕生了實現直譯器在執行時採用逐行解釋位元組碼執行程式的想法。
直譯器真正意義上所承擔的角色就是一個執行時「翻譯者」,將位元組碼檔案中的內容「翻譯」爲對應平臺的本地機器指令執行。
當一條位元組碼指令被解釋執行完成後,接着再根據PC暫存器中記錄的下一條需要被執行的位元組碼指令執行解釋操作。
分類
在Java的發展歷史裏,一共有兩套解釋執行器,即古老的位元組碼直譯器、現在普遍使用的模板直譯器。
位元組碼直譯器在執行時通過純軟體程式碼模擬位元組碼的執行,效率非常低下。
而模板直譯器將每一條位元組碼和一個模板函數相關聯,模板函數中直接產生這條位元組碼執行時的機器碼,從而很大程度上提高瞭直譯器的效能。
在HotSpot VM中,直譯器主要由Interpreter
模組和Code
模組構成。
Interpreter
模組:實現瞭直譯器的核心功能Code
模組:用於管理HotSpot VM在執行時生成的本地機器指令現狀
由於直譯器在設計和實現上非常簡單,因此除了Java語言之外,還有許多高階語言同樣也是基於直譯器執行的,比如Python、Perl、Ruby等。
但是在今天,基於直譯器執行已經淪落爲低效的代名詞,並且時常被一些C/C++程式設計師所調侃。
爲了解決這個問題,JVM平臺支援一種叫作即時編譯的技術。即時編譯的目的是避免函數被解釋執行,而是將整個函數體編譯成爲機器碼,每次函數執行時,只執行編譯後的機器碼即可,這種方式可以使執行效率大幅度提升。
不過無論如何,基於直譯器的執行模式仍然爲中間語言的發展做出了不可磨滅的貢獻。
第一種是將原始碼編譯成位元組碼檔案,然後在執行時通過直譯器將位元組碼檔案轉爲機器碼執行
第二種是編譯執行(直接編譯成機器碼)。現代虛擬機器爲了提高執行效率,會使用即時編譯技術(JIT,Just In Time)將方法編譯成機器碼後再執行
HotSpot VM是目前市面上高效能虛擬機器的代表作之一。它採用直譯器與即時編譯器並存的架構。在Java虛擬機器執行時,直譯器和即時編譯器能夠相互共同作業,各自取長補短,盡力去選擇最合適的方式來權衡編譯原生代碼的時間和直接解釋執行程式碼的時間。
在今天,Java程式的執行效能早已脫胎換骨,已經達到了可以和C/C++ 程式一較高下的地步。
有些開發人員會感覺到詫異,既然 HotSpot VM 中已經內建 JIT編譯器了,那麼爲什麼還需要再使用直譯器來「拖累」程式的執行效能呢?
JRockit 虛擬機器是砍掉瞭直譯器,也就是隻採及時編譯器。那是因爲呢JRockit只部署在伺服器上,一般已經有時間讓他進行指令編譯的過程了,對於響應來說要求不高,等及時編譯器的編譯完成後,就會提供更好的效能
首先明確: 當程式啓動後,直譯器可以馬上發揮作用,省去編譯的時間,立即執行。 編譯器要想發揮作用,把程式碼編譯成原生代碼,需要一定的執行時間。但編譯爲原生代碼後,執行效率高。
所以: 儘管 JRockit VM 中程式的執行效能會非常高效,但程式在啓動時必然需要花費更長的時間來進行編譯。對於伺服器端應用來說,啓動時間並非是關注重點,但對於那些看中啓動時間的應用場景而言,或許就需要採用直譯器與即時編譯器並存的架構來換取一個平衡點。
在此模式下,當Java虛擬器啓動時,直譯器可以首先發揮作用,而不必等待即時編譯器全部編譯完成後再執行,這樣可以省去許多不必要的編譯時間。隨着時間的推移,編譯器發揮作用,把越來越多的程式碼編譯成原生代碼,獲得更高的執行效率。
同時,解釋執行在編譯器進行激進優化不成立的時候,作爲編譯器的「逃生門」。
當虛擬機器啓動的時候,直譯器可以首先發揮作用,而不必等待即時編譯器全部編譯完成再執行,這樣可以省去許多不必要的編譯時間。並且隨着程式執行時間的推移,即時編譯器逐漸發揮作用,根據熱點探測功能,將有價值的位元組碼編譯爲本地機器指令,以換取更高的程式執行效率。
案例
注意 解釋執行 與 編譯執行 在線上環境微妙的辯證關係。機器在熱機狀態可以承受的負載要大於冷機狀態。如果以熱機狀態時的流量進行切流,可能使處於冷機狀態的伺服器因無法承載流量而假死。
在生產環境發佈過程中,以分批的方式進行發佈,根據機器數量劃分成多個批次,每個批次的機器數至多佔到整個叢集的1/8。曾經有這樣的故障案例:某程式設計師在發佈平臺進行分批發布,在輸入發佈總批數時,誤填寫成分爲兩批發布。如果是熱機狀態,在正常情況下一半的機器可以勉強承載流量,但由於剛啓動的JVM均是解釋執行,還沒有進行熱點程式碼統計和JIT動態編譯,導致機器啓動之後,當前1/2發佈成功的伺服器馬上全部宕機,此故障說明了JIT的存在。—阿裡團隊
Java 語言的「編譯期」其實是一段「不確定」的操作過程,
.java
檔案轉變成.class
檔案的過程;.java
檔案編譯成本地機器程式碼的過程。不同的編譯器應用
一個被多次呼叫的方法,或者是一個方法體內部回圈次數較多的回圈體都可以被稱之爲「熱點程式碼」,因此都可以通過JIT編譯器編譯爲本地機器指令。
On Stack Replacement
)編譯。一個方法究竟要被呼叫多少次,或者一個回圈體究竟需要執行多少次回圈纔可以達到這個標準?
目前HotSpot VM所採用的熱點探測方式是基於計數器的熱點探測。
這個計數器就用於統計方法被呼叫的次數。
這個閥值可以通過虛擬機器參數 -XX:CompileThreshold
來人爲設定。
當一個方法被呼叫時,會先檢查該方法是否存在被JIT編譯過的版本
然後判斷方法呼叫計數器與回邊計數器值之和是否超過方法呼叫計數器的閥值
如果不做任何設定,方法呼叫計數器統計的並不是方法被呼叫的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被呼叫的次數
。
當超過一定的時間限度,如果方法的呼叫次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的呼叫計數器就會被減少一半,這個過程稱爲方法呼叫計數器熱度的衰減(Counter Decay),而這段時間就稱爲此方法統計的半衰週期(Counter Half Life Time)
進行熱度衰減的動作是在虛擬機器進行垃圾收集時順便進行的,可以使用虛擬機器參數 -XX:-UseCounterDecay
來關閉熱度衰減,讓方法計數器統計方法呼叫的絕對次數,這樣,只要系統執行時間足夠長,絕大部分方法都會被編譯成原生代碼。
-XX:CounterHalfLifeTime
參數設定半衰週期的時間,單位是秒。它的作用是統計一個方法中回圈體程式碼執行的次數,在位元組碼中遇到控制流向後跳轉的指令稱爲「回邊」(Back Edge)。
預設情況下HotSpot VM是採用直譯器與即時編譯器並存的架構。
當然開發人員可以根據具體的應用場景,通過命令顯式地爲Java虛擬機器指定在執行時到底是完全採用直譯器執行,還是完全採用即時編譯器執行。如下所示:
-Xint
:完全採用直譯器模式執行程式;-Xcomp
:完全採用即時編譯器模式執行程式。如果即時編譯出現問題,直譯器會介入執行Xmixed
:採用直譯器+即時編譯器的混合模式共同執行程式。JIT的編譯器還分爲了兩種,分別是C1
和C2
,在 HotSpot VM 中內嵌有兩個JIT編譯器,分別爲Client Compiler
和Server Compiler
,但大多數情況下我們簡稱爲C1編譯器
和 C2編譯器
。
開發人員可以通過如下命令顯式指定Java虛擬機器在執行時到底使用哪一種即時編譯器,如下所示:
-client
:指定Java虛擬機器執行在Client模式
下,並使用C1編譯器;
-server
:指定Java虛擬機器執行在Server模式
下,並使用C2編譯器。
在不同的編譯器上有不同的優化策略,C1編譯器上主要有方法內聯,去虛擬化、元餘消除。
C2的優化主要是在全域性層面,逃逸分析是優化的基礎。
基於逃逸分析在C2上有如下幾種優化:
synchronized
分層編譯策略
分層編譯(Tiered Compilation)策略:程式解釋執行(不開啓效能監控)可以觸發C1編譯,將位元組碼編譯成機器碼,可以進行簡單優化;也可以加上效能監控,C2編譯會根據效能監控資訊進行激進優化。
不過在Java7版本之後,一旦開發人員在程式中顯式指定命令「-server"時,預設將會開啓分層編譯策略,由 C1編譯器 和 C2編譯器 相互共同作業共同來執行編譯任務。
總結
JDK9 引入了 AOT 編譯器(靜態提前編譯器,Ahead of Time Compiler
)
Java 9 引入了實驗性AOT編譯工具jaotc
。它藉助了Graal編譯器,將所輸入的Java類檔案轉換爲機器碼,並存放至生成的動態共用庫之中。
.java -> .class -> (使用jaotc) -> .so
所謂AOT編譯,是與即時編譯相對立的一個概念。
我們知道,即時編譯指的是在程式的執行過程中,將位元組碼轉換爲可在硬體上直接執行的機器碼,並部署至託管環境中的過程。
而AOT編譯指的則是,在程式執行之前,便將位元組碼轉換爲機器碼的過程。
最大的好處:
缺點:
自JDK10起,HotSpot 又加入了一個全新的及時編譯器:Graal編譯器
編譯效果短短幾年時間就追平了G2編譯器,未來可期
目前,帶着實驗狀態標籤,需要使用開關參數去啓用才能 纔能使用
-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler