執行引擎概述
- 執行引擎是Java虛擬機的核心組成部分之一
- 虛擬機是一個相對於「物理機」的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接創建在處理器、緩存、指令集和操做系統層面上的,而虛擬機的執行引擎則是由軟件自行實現的,所以能夠不受物理條件制約地定製指令集與執行引擎的結構體系,可以執行那些不被硬件直接支持的指令集格式。
- JVM的主要任務是==負責裝載字節碼到其內部==,但字節碼並不可以直接運行在操做系統之上,由於字節碼指令並不是等價於本地機器指令,它內部包含的僅僅只是一些可以被JVM鎖識別的字節碼指令、符號表和其餘輔助信息
- 那麼,若是想讓一個Java程序運行起來、執行引擎的任務就是==將字節碼指令解釋/編譯爲對應平臺上的本地機器指令才能夠==。簡單來講,JVM中的執行引擎充當了將高級語言翻譯爲機器語言的譯者.
- 執行引擎的工做過程
- 從外觀上來看,全部的Java虛擬機的執行引擎輸入、輸出都是一致的:輸入的是字節碼二進制流,處理過程是字節碼解析執行的等效過程,輸出的是執行結果。
1)執行引擎在執行的過程當中究竟須要執行什麼樣的字節碼指令徹底依賴於PC寄存器。
2)每當執行完一項指令操做後,PC寄存器就會更新下一條須要被執行的指令地址。
3)固然方法在執行的過程當中,執行引擎有可能會經過存儲在局部變量表中的對象引用準肯定位到存儲在Java堆區中的對象實例信息,以及經過對象頭中的元數據指針定位到目標對象的類型信息。
Java代碼編譯和執行過程
大部分的程序代碼轉換成物理機的目標代碼或虛擬機能執行的指令集以前,都須要通過下面圖中的各個步驟:
Java代碼編譯是由Java源碼編譯器來完成,流程圖以下所示:
Java字節碼的執行是由JVM執行引擎來完成,流程圖以下所示:
前端
什麼是解釋器( Interpreter),什麼是JIT編譯器?
解釋器:當Java虛擬機啓動時會根據預約義的規範對字節碼採用逐行解釋的方式執行,將每條字節碼文件中的內容「翻譯」爲對應平臺的本地機器指令執行。
JIT (Just In Time Compiler)編譯器(即時編譯器):就是虛擬機將源代碼直接編譯成和本地機器平臺相關的機器語言。java
爲何說Java是半編譯半解釋型語言?
JDK1.0時代,將Java語言定位爲「解釋執行」仍是比較準確的。再後來,Java也發展出能夠直接生成本地代碼的編譯器。
如今JVM在執行Java代碼的時候,一般都會將解釋執行與編譯執行兩者結合起來進行。
git
機器碼、指令、彙編語言
機器碼
- 各類用二進制編碼方式表示的指令,叫作==機器指令碼==。開始,人們就用它採編寫程序,這就是機器語言。
- 機器語言雖然可以被計算機理解和接受,但和人們的語言差異太大,不易被人們理解和記憶,而且用它編程容易出差錯。
- 用它編寫的程序一經輸入計算機,CPU直接讀取運行,所以和其餘語言編的程序相比,執行速度最快。
- 機器指令與CPU緊密相關,因此不一樣種類的CPU所對應的機器指令也就不一樣。
指令
- 因爲機器碼是有0和1組成的二進制序列,可讀性實在太差,因而人們發明了指令。
- 指令就是把機器碼中特定的0和1序列,簡化成對應的指令(通常爲英文簡寫,如mov,inc等),可讀性稍好
- 因爲不一樣的硬件平臺,執行同一個操做,對應的機器碼可能不一樣,因此不一樣的硬件平臺的同一種指令(好比mov),對應的機器碼也可能不一樣。
指令集
- 不一樣的硬件平臺,各自支持的指令,是有差異的。所以每一個平臺所支持的指令,稱之爲對應平臺的指令集。
- 如常見的
- x86指令集,對應的是x86架構的平臺
- ARM指令集,對應的是ARM架構的平臺
彙編語言
- 因爲指令的可讀性仍是太差,因而人們又發明了彙編語言。
- 在彙編語言中,用助記符(Mnemonics)代替機器指令的操做碼,用地址符號(Symbol)或標號(Label)代替指令或操做數的地址。
- 在不一樣的硬件平臺,彙編語言對應着不一樣的機器語言指令集,經過彙編過程轉換成機器指令。
- 因爲計算機只認識指令碼,因此用匯編語言編寫的程序還必須翻譯成機器指令碼,計算機才能識別和執行。
高級語言
- 爲了使計算機用戶編程序更容易些,後來就出現了各類高級計算機語言。高級語言比機器語言、彙編語言更接近人的語言
- 當計算機執行高級語言編寫的程序時,仍然須要把程序解釋和編譯成機器的指令碼。完成這個過程的程序就叫作解釋程序或編譯程序。
![6](http://static.javashuo.com/static/loading.gif)
字節碼
- 字節碼是一種中間狀態(中間碼)的二進制代碼(文件),它比機器碼更抽象,須要直譯器轉譯後才能成爲機器碼
- 字節碼主要爲了實現特定軟件運行和軟件環境、與硬件環境無關。
- 字節碼的實現方式是經過編譯器和虛擬機器。編譯器將源碼編譯成字節碼,特定平臺上的虛擬機器將字節碼轉譯爲能夠直接執行的指令。
C、C++源程序執行過程
編譯過程又能夠分紅兩個階段:編譯和彙編。程序員
- 編譯過程:是讀取源程序(字符流),對 之進行詞法和語法的分析,將高級語言指令轉換爲功能等效的彙編代碼
- 彙編過程:實際上指把彙編語言代碼翻譯成目標機器指令的過程。
![](http://static.javashuo.com/static/loading.gif)
解釋器
JVM設計者們的初衷僅僅只是單純地爲了==知足Java程序實現跨平臺特性==,所以避免採用靜態編譯的方式直接生成本地機器指令,從而誕生了實現解釋器在運行時採用逐行解釋字節碼執行程序的想法。
github
- 解釋器真正意義上所承擔的角色就是一個運行時「翻譯者」,將字節碼文件中的內容「翻譯」爲對應平臺的本地機器指令執行。
- 當一條字節碼指令被解釋執行完成後,接着再根據PC寄存器中記錄的下一條須要被執行的字節碼指令執行解釋操做。
在Java的發展歷史裏,一共有兩套解釋執行器,即古老的==字節碼解釋器==、如今廣泛使用的==模板解釋器==。算法
- 字節碼解釋器在執行時經過純軟件代碼模擬字節碼的執行,效率很是低下。· - 而模板解釋器將每一 條字節碼和一個模板函數相關聯,模板函數中直接產生這條字節碼執行時的機器碼,從而很大程度上提升瞭解釋器的性能。
- 在HotSpot VM中,解釋器主要由Interpreter模塊和Code模塊構成。
- Interpreter模塊:實現瞭解釋器的核心功能
- Code模塊:用於管理HotSpot VM在運行時生成的本地機器指令
現狀
- 因爲解釋器在設計和實現上很是簡單,所以除了Java語言以外,還有許多高級語言一樣也是基於解釋器執行的,好比Python、 Perl、Ruby等。可是在今天,基於解釋器執行已經淪落爲低效的代名詞,而且時常被一些C/C+ +程序員所調侃。
- 爲了解決這個問題,JVM平臺支持一種叫做即時編譯的技術。即時編譯的目的是避免函數被解釋執行,而是將整個函數體編譯成爲機器碼,每次函數執行時,只執行編譯後的機器碼便可,這種方式可使執行效率大幅度提高。
- 不過不管如何,基於解釋器的執行模式仍然爲中間語言的發展作出了不可磨滅的貢獻。
JIT編譯器
HotSpot VM 爲什麼解釋器與JIT編譯器共存
java代碼的執行分類:編程
- 第一種是將源代碼編譯成字節碼文件,而後再運行時經過解釋器將字節碼文件轉爲機器碼執行
- 第二種是編譯執行(直接編譯成機器碼)。現代虛擬機爲了提升執行效率,會使用即時編譯技術(JIT,Just In Time)將方法編譯成機器碼後再執行
HotSpot VM是目前市面上高性能虛擬機的表明做之一。它採用==解釋器與即時編譯器並存的架構==。在Java虛擬機運行時,解釋器和即時編譯器可以相互協做,各自取長補短,盡力去選擇最合適的方式來權衡編譯本地代碼的時間和直接解釋執行代碼的時間。
在今天,Java程序的運行性能早已脫胎換骨,已經達到了能夠和C/C++程序一較高下的地步。後端
解釋器依然存在的必要性
有些開發人員會感受到詫異,既然HotSpotVM中已經內置JIT編譯器了,那麼爲何還須要再使用解釋器來「拖累」程序的執行性能呢?好比JRockit VM內部就不包含解釋器,字節碼所有都依靠即時編譯器編譯後執行。緩存
首先明確:
當程序啓動後,解釋器能夠立刻發揮做用,省去編譯的時間,當即執行。 編譯器要想發揮做用,把代碼編譯成本地代碼,須要必定的執行時間。但編譯爲本地代碼後,執行效率高。bash
因此:
儘管JRockitVM中程序的執行性能會很是高效,但程序在啓動時必然須要花費更長的時間來進行編譯。對於服務端應用來講,啓動時間並不是是關注重點,但對於那些看中啓動時間的應用場景而言,或許就須要採用解釋器與即時編譯器並存的架構來換取一一個平衡點。在此模式下,==當Java虛擬器啓動時,解釋器能夠首先發揮做用,而沒必要等待即時編譯器所有編譯完成後再執行,這樣能夠省去許多沒必要要的編譯時間。隨着時間的推移,編譯器發揮做用,把愈來愈多的代碼編譯成本地代碼,得到更高的執行效率。==
同時,解釋執行在編譯器進行激進優化不成立的時候,做爲編譯器的「逃生門」。
HostSpot JVM的執行方式
當虛擬機啓動的時候,解釋器能夠首先發揮做用,而沒必要等待即時編譯器所有編譯完成再執行,這樣能夠省去許多沒必要要的編譯時間。而且隨着程序運行時間的推移,即時編譯器逐漸發揮做用,==根據熱點探測功能,將有價值的字節碼編譯爲本地機器指令,以換取更高的程序執行效率。==
案例
注意解釋執行與編譯執行在線上環境微妙的辯證關係。機器在熱機狀態能夠承受的負載要大於冷機狀態。若是以熱機狀態時的流量進行切流,可能使處於冷機狀態的服務器因沒法承載流量而假死。
在生產環境發佈過程當中,以分批的方式進行發佈,根據機器數量劃分紅多個批次,每一個批次的機器數至多佔到整個集羣的1/8。曾經有這樣的故障案例:某程序員在發佈平臺進行分批發布,在輸入發佈總批數時,誤填寫成分爲兩批發布。若是是熱機狀態,在正常狀況下一半的機器能夠勉強承載流量,但因爲剛啓動的JVM均是解釋執行,尚未進行熱點代碼統計和JIT動態編譯,致使機器啓動以後,當前1/2發佈成功的服務器立刻所有宕機,此故障說明了JIT的存在。一阿里團隊
![9](http://static.javashuo.com/static/loading.gif)
JIT編譯器
概念解釋
- Java 語言的「編譯器」 實際上是一段「不肯定」的操做過程,由於它多是指一個==前端編譯器==(其實叫「編譯器的前端」 更準確一些)把.java文件轉變成.class文件的過程;
- 也多是指虛擬機的==後端運行期編譯器==(JIT 編譯器,Just In Time Compiler)把字節碼轉變成機器碼的過程。
- 還多是指使用==靜態提早編譯器==(AOT 編譯器,Ahead Of Time Compiler)直接把. java文件編譯成本地機器代碼的過程。
前端編譯器: Sun的Javac、 Eclipse JDT中的增量式編譯器(ECJ)
JIT編譯器: HotSpot VM的C一、C2編譯器。
AOT編譯器: GNU Compiler for the Java (GCJ) 、Excelsior JET。
熱點代碼及探測方式
固然是否須要啓動JIT編譯器將字節碼直接編譯爲對應平臺的本地機器指令,則須要根據代碼被調用執行的頻率而定。關於那些須要被編譯爲本地代碼的字節碼,也被稱之爲「熱點代碼」 ,JIT編譯器在運行時會針對那些頻繁被調用的「熱點代碼」作出深度優化,將其直接編譯爲對應平臺的本地機器指令,以此提高Java程序的執行性能。
- 一個被屢次調用的方法,或者是一個方法體內部循環次數較多的循環體均可以被稱之爲「熱點代碼」,所以均可以經過JIT編譯器編譯爲本地機器指令。因爲這種編譯方式發生在方法的執行過程當中,所以也被稱之爲棧上替換,或簡稱爲OSR (On StackReplacement)編譯。
- 一個方法究竟要被調用多少次,或者一個循環體究竟須要執行多少次循環才能夠達到這個標準?必然須要一個明確的閾值,JIT編譯器纔會將這些「熱點代碼」編譯爲本地機器指令執行。這裏主要依靠==熱點探測功能==。
- ==目前HotSpot VM所採用的熱點探測方式是基於計數器的熱點探測==。
- 採用基於計數器的熱點探測,HotSpot VM將會爲每個 方法都創建2個不一樣類型的計數器,分別爲方法調用計數器(Invocation Counter) 和回邊計數器(BackEdge Counter) 。
- 方法調用計數器用於統計方法的調用次數
- 回邊計數器則用於統計循環體執行的循環次數
方法調用計數器
- 這個計數器就用於統計方法被調用的次數,它的默認閾值在Client 模式 下是1500 次,在Server 模式下是10000 次。超過這個閾值,就會觸發JIT編譯。
- 這個閾值能夠經過虛擬機參數一XX :CompileThreshold來人爲設定。
- 當一個方法被調用時, 會先檢查該方法是否存在被JIT編譯過的版本,如 果存在,則優先使用編譯後的本地代碼來執行。若是不存在已被編譯過的版本,則將此方法的調用計數器值加1,而後判斷方法調用計數器與回邊計數器值之和是否超過方法調用計數器的閾值。若是已超過閾值,那麼將會向即時編譯器提交一個該方法的代碼編譯請求。
![10](http://static.javashuo.com/static/loading.gif)
熱度衰減
- 若是不作任何設置,方法調用計數器統計的並非方法被調用的絕對次數,而是一一個相對的執行頻率,即一段時間以內方法被調用的次數。當超過必定的時間限度, 若是方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減小一半,這個過程稱爲方法調用計數器熱度的衰減(Counter Decay) ,而這段時間就稱爲此方法統計的半衰週期(Counter Half Life Time)。
- 進行熱度衰減的動做是在虛擬機進行垃圾收集時順便進行的,可使用虛擬機參數 -XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法調用的絕對次數,這樣,只要系統運行時間足夠長,絕大部分方法都會被編譯成本地代碼。
- 另外, 可使用-XX: CounterHalfLifeTime參數設置半衰週期的時間,單位是秒。
回邊計數器
它的做用是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲「回邊」 (Back Edge)。顯然,創建回邊計數器統計的目的就是爲了觸發OSR編譯。
![11](http://static.javashuo.com/static/loading.gif)
HotSpot VM 能夠設置程序執行方式
缺省狀況下HotSpot VM是採用解釋器與即時編譯器並存的架構,固然開發人員能夠根據具體的應用場景,經過命令顯式地爲Java虛擬機指定在運行時究竟是徹底採用解釋器執行,仍是徹底採用即時編譯器執行。以下所示:
- -Xint: 徹底採用解釋器模式執行程序;
- -Xcomp: 徹底採用即時編譯器模式執行程序。若是即時編譯出現問題,解釋器會介入執行。
- -Xmixed:採用解釋器+即時編譯器的混合模式共同執行程序。
![](http://static.javashuo.com/static/loading.gif)
測試解釋器模式和JIT編譯模式
測試代表:
- 純解釋器模式速度最慢(JVM1.0版本用的就是純解釋器執行)
- 混合模式速度更快
/**
* 測試解釋器模式和JIT編譯模式
* -Xint : 6520ms
* -Xcomp : 950ms
* -Xmixed : 936ms
*/
public class IntCompTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
testPrimeNumber(1000000);
long end = System.currentTimeMillis();
System.out.println("花費的時間爲:" + (end - start));
}
public static void testPrimeNumber(int count){
for (int i = 0; i < count; i++) {
//計算100之內的質數
label:for(int j = 2;j <= 100;j++){
for(int k = 2;k <= Math.sqrt(j);k++){
if(j % k == 0){
continue label;
}
}
//System.out.println(j);
}
}
}
}
複製代碼
HotSpot VM 中的JIT分類
在HotSpot VM中內嵌有兩個JIT編譯器,分別爲Client Compiler和Server Compiler,但大多數狀況下咱們簡稱爲C1編譯器和C2編譯器。開發人員能夠經過以下命.令顯式指定Java虛擬機在運行時到底使用哪種即時編譯器,以下所示:
- -client: 指定Java虛擬機運行在Client模式下,並使用C1編譯器;
- C1編譯器會對字節碼進行==簡單和可靠的優化,耗時短==。以達到更快的編譯速度。
- -server: 指定Java虛擬機運行在Server模式下,並使用C2編譯器。
- C2進行==耗時較長的優化,以及激進優化==。但優化的代碼執行效率更高。
C1和C2編譯器不一樣的優化策略
- 在不一樣的編譯器上有不一樣的優化策略,C1編譯器上主要有方法內聯,去虛擬化、冗餘消除。
- 方法內聯:將引用的函數代碼編譯到引用點處,這樣能夠減小棧幀的生成,減小參數傳遞以及跳轉過程
- 去虛擬化:對惟一的實現類進行內聯
- 冗餘消除:在運行期間把一些不會執行的代碼摺疊掉
- C2的優化主要是在全局層面,逃逸分析是優化的基礎。基於逃逸分析在C2.上有以下幾種優化:(server模式下才會有這些優化,64位系統默認就是server模式)
- 標量替換:用標量值代替聚合對象的屬性值
- 棧上分配:對於未逃逸的對象分配對象在棧而不是堆
- 同步消除:清除同步操做,一般指synchronized
分層編譯(Tiered Compilation)策略:程序解釋執行(不開啓性能監控)能夠觸發C1編譯,將字節碼編譯成機器碼,能夠進行簡單優化,也能夠加上性能監控,C2編譯會根據性能監控信息進行激進優化。
不過在Java7版本以後,一旦開發人員在程序中顯式指定命令「一server"時,默認將會開啓分層編譯策略,由C1編譯器和C2編譯器相互協做共同來執行編譯任務。
總結
- 通常來說,JIT編譯出來的機器碼性能比解釋器高。
- C2編譯器啓動時長比C1編譯器慢,系統穩定執行之後,C2編譯器執行速度遠遠快於C1編譯器。
Graal編譯器與AOT編譯器
Graal編譯器
- 自JDK10起,HotSpot又加入一個全新的即時編譯器: Graal編譯器
- 編譯效果短短几年時間就追評了C2編譯器。將來可期。
- 目前,帶着「實驗狀態"標籤,須要使用開關參數 -XX: +UnlockExperimentalVMOptions 一XX: +UseJVMCICompiler去激活,纔可使用。
AOT編譯器
- jdk9引入了AOT編譯器(靜態提早編譯器,Ahead Of Time Compiler)
- Java 9引入了實驗性AOT編譯工具jaotc。它藉助了Graal 編譯器,將所輸入的Java 類文件轉換爲機器碼,並存放至生成的動態共享庫之中。
- 所謂AOT編譯,是與即時編譯相對立的一個概念。咱們知道,即時編譯指的是在程序的運行過程當中,將字節碼轉換爲可在硬件上直接運行的機器碼,並部署至託管環境中的過程。而AOT編譯指的則是,在程序運行以前,便將字節碼轉換爲機器碼的過程。
- 最大好處: Java虛擬機加載已經預編譯成二進制庫,能夠直接執行。沒必要等待即時編譯器的預熱,減小Java應用給人帶來「第一次運行慢」的不良體驗。
- 缺點:
- 破壞了java"一次編譯,處處運行」,必須爲每一個不一樣硬件、oS編譯對應的發行包。
- 下降了Java連接過程的動態性,加載的代碼在編譯期就必須所有已知。
- 還須要繼續優化中,最初只支持Linux x64 java base
JVM學習代碼及筆記(陸續更新中...)
【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器