1. 執行引擎概述和基本執行程序
1.1 概述
在前面的博文中, 已經對 類的加載,和加載后類的運行時資料區進行說明,但是 我們的java程式是運行在jvm中的,而我們的位元組碼指令并不能直接被作業系統所識別, 這個時候就需要 執行引擎登場了.

- 執行引擎是Java虛擬機核心的組成部分之一,“虛擬機”是一個相對于“物理機”的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、快取、指令集和作業系統層面上的,而虛擬機的執行引擎則是由軟體自行實作的,因此可以不受物理條件制約地定制指令集與執行引擎的結構體系,能夠執行那些不被硬體直接支持的指令集格式,
- JVM的主要任務是負責裝載位元組碼到其內部,但位元組碼并不能夠直接運行在作業系統之上,因為位元組碼指令并非等價于本地機器指令,它內部包含的僅僅只是一些能夠被JVM所識別的位元組碼指令、符號表,以及其他輔助資訊,
- 那么,如果想要讓一個Java程式運行起來,執行引擎(Execution Engine)的任務就是將位元組碼指令解釋/編譯為對應平臺上的本地機器指令才可以,簡單來說,JVM中的執行引擎充當了將高級語言翻譯為機器語言的譯者,

1.2 基本執行程序
在前面我們學到了 執行緒中獨有的 程式計數器,記錄下一行 指令的地址,提供給執行引擎執行,
- 執行引擎在執行的程序中究竟需要執行什么樣的位元組碼指令完全依賴于PC暫存器,
- 每當執行完一項指令操作后,PC暫存器就會更新下一條需要被執行的指令地址,
- 當然方法在執行的程序中,執行引擎有可能會通過存盤在區域變數表中的物件參考準確定位到存盤在Java堆區中的物件實體資訊,以及通過物件頭中的元資料指標定位到目標物件的型別資訊,
- 從外觀上來看,所有的Java虛擬機的執行引擎輸入、處理、輸出都是一致的:輸入的是位元組碼二進制流,處理程序是位元組碼決議執行、即時編譯的等效程序,輸出的是執行程序,
示意圖:

2. java代碼編譯和執行的程序
2.1 Java代碼的解釋執行 和 編譯執行
大部分的程式代碼轉換成物理機的目標代碼或虛擬機能執行的指令集之前,都需要經過下圖中的各個步驟:

- 前面橙色部分是編譯生成生成位元組碼檔案的程序(javac 指令java檔案轉class檔案),和JVM無關
- 后面綠色(解釋執行)和藍色(即時編譯)才是JVM執行執行的程序,對應著執行引擎中的解釋器和編譯器
什么是解釋器?什么是JIT編譯器?
- **解釋器: **
當Java虛擬機啟動時會根據預定義的規范對位元組碼采用逐行解釋的方式執行,將每條位元組碼檔案中的內容“翻譯”為對應平臺的本地機器指令執行,
- JIT(Just In Time Compiler)編譯器
就是虛擬機將源代碼直接編譯成和本地機器平臺相關的機器語言,
在初學java時, 我們稱呼java語言為 半編譯型半解釋型語言, 這是因為jvm中 執行引擎的作業就是這樣的,
- JDK1.0時代,將Java語言定位為“解釋執行”還是比較準確的,再后來,Java也發展出可以直接生成本地代碼的編譯器,
- 現在JVM在執行Java代碼的時候,通常都會將解釋執行與編譯執行二者結合起來進行,
- 將位元組碼翻譯成本地代碼后,就可以做一個快取操作,存盤在方法區的JIT 代碼快取中
jvm執行程序總結圖:

2.2 機器碼,指令,匯編語言
機器碼
- 各種用二進制編碼方式表示的指令,叫做機器指令碼(就是一串010101,可以直接被cpu執行),開始,人們就用它采撰寫程式,這就是機器語言,
- 機器語言雖然能夠被計算機理解和接受,但和人們的語言差別太大,不易被人們理解和記憶,并且用它編程容易出差錯,
- 用它撰寫的程式一經輸入計算機,CPU直接讀取運行,因此和其他語言編的程式相比,執行速度最快,
- 機器指令與CPU緊密相關,所以不同種類的CPU所對應的機器指令也就不同,
指令
-
由于機器碼是由0和1組成的二進制序列,可讀性實在太差,于是人們發明了指令,
-
指令就是把機器碼中特定的0和1序列,簡化成對應的指令(一般為英文簡寫,如mov,inc等),可讀性稍好
一個指令可能就代表一大串連續功能的機器碼
-
由于不同的硬體平臺,執行同一個操作,對應的機器碼可能不同,所以不同的硬體平臺的同一種指令(比如mov),對應的機器碼也可能不同,
-
不同的硬體平臺,各自支持的指令,是有差別的,因此每個平臺所支持的指令,稱之為對應平臺的指令集,
- x86指令集,對應的是x86架構的平臺
- ARM指令集,對應的是ARM架構的平臺
匯編語言
- 由于指令的可讀性還是太差,于是人們又發明了匯編語言,
- 在匯編語言中,用助記符(Mnemonics)代替機器指令的操作碼,用地址符號(Symbol)或標號(Label)代替指令或運算元的地址,在不同的硬體平臺,匯編語言對應著不同的機器語言指令集,通過匯編程序轉換成機器指令,
- 由于計算機只認識機器碼,所以用匯編語言撰寫的程式還必須翻譯(匯編)成機器碼,計算機才能識別和執行,
高級語言
- 為了使計算機用戶編程式更容易些,后來就出現了各種高級計算機語言,(C,C++,JAVA)
- 高級語言比機器語言、匯編語言更接近人的語言當計算機執行高級語言撰寫的程式時,仍然需要把程式解釋和編譯成機器的指令碼,完成這個程序的程式就叫做解釋程式或編譯程式,
- 高級語言也不是直接翻譯成 機器指令
- 首先需要通過編譯階段,將高級語言翻譯為為匯編語言
- 再通過匯編階段,將生成的匯編語言翻譯成機器指令
C 語言的流程:
- 編譯程序:是讀取源程式(字符流),對之進行詞法和語法的分析,將高級語言指令轉換為功能等效的匯編代碼
- 匯編程序:實際上指把匯編語言代碼翻譯成目標機器指令的程序,

JAVA 語言也是差不多的,首先將撰寫的JAVA檔案編譯為位元組碼檔案(也稱為前端編譯,完成編譯作業的一半,減輕jvm編譯的作業量),也就是我們前面經常看到的位元組碼指令,下面是位元組碼的具體說明:
- 位元組碼是一種中間狀態(中間碼)的二進制代碼(檔案),它比機器碼更抽象,需要執行引擎轉譯后才能成為機器碼
- 位元組碼主要為了實作特定軟體運行和軟體環境、與硬體環境無關,
- 位元組碼的實作方式是通過編譯器和虛擬機器,編譯器將原始碼編譯成位元組碼,特定平臺上的虛擬機器將位元組碼轉譯為可以直接執行的指令,
3. 解釋器
3.1 為什么要有位元組碼這種中間碼
在前面說到, jvm中的執行引擎可以將 位元組碼 指令進行解釋執行, 那同樣的道理,為什么 解釋器不能直接執行java原始碼呢,
理論上是可以的

上面的圖中表示, java語言在到jvm 的前一步,還將java原始碼編譯了一下,成為 class位元組碼, 這一步將大大節省了jvm的作業,javac 將java語言編譯為更加底層的指令形式,并在編譯程序中進行了優化 (字串,常量等優化),
3.2 解釋器的作用
- JVM設計者們的初衷僅僅只是單純地為了滿足Java程式實作跨平臺特性,因此避免采用靜態編譯的方式直接生成本地機器指令,從而誕生了實作解釋器在運行時采用逐行解釋位元組碼執行程式的想法,
- 解釋器真正意義上所承擔的角色就是一個運行時“翻譯者”,將位元組碼檔案中的內容“翻譯”為對應平臺的本地機器指令執行,
- 當一條位元組碼指令被解釋執行完成后,接著再根據PC暫存器中記錄的下一條需要被執行的位元組碼指令執行解釋操作,
3.3 解釋器的分類
- 在Java的發展歷史里,一共有兩套解釋執行器,即古老的位元組碼解釋器和現在普遍使用的模板解釋器,
- 位元組碼解釋器在執行時通過純軟體代碼模擬位元組碼的執行,效率非常低下,
- 而模板解釋器將每一條位元組碼和一個模板函式相關聯,模板函式中直接產生這條位元組碼執行時的機器碼,從而很大程度上提高了解釋器的性能,(Map鍵值對?)
如今在HotSpot VM中,解釋器主要由Interpreter模塊和Code模塊構成,
- Interpreter模塊:實作了解釋器的核心功能
- Code模塊:用于管理HotSpot VM在運行時生成的本地機器指令
3.4 解釋器的現狀
- 由于解釋器在設計和實作上非常簡單,因此除了Java語言之外,還有許多高級語言同樣也是基于解釋器執行的,比如Python、Perl、Ruby等,但是在今天,基于解釋器執行已經淪落為低效的代名詞,并且時常被一些C/C++程式員所調侃,
- 為了解決這個問題,JVM平臺支持一種叫作即時編譯的技術,即時編譯的目的是避免函式被解釋執行,而是將整個函式體編譯成為機器碼,每次函式執行時,只執行編譯后的機器碼即可,這種方式可以使執行效率大幅度提升,
- 不過無論如何,基于解釋器的執行模式仍然為中間語言的發展做出了不可磨滅的貢獻,
就是如今, 解釋器在執行引擎中也是不可或缺的一部分, 雖然即時編譯 有很多優點,但是它也有它的缺點, 需要解釋器的補充, 要兩個在一起,才是最好 (益達打錢)
4. JIT編譯器
4.1 . 概述
- HotSpot VM是目前市面上高性能虛擬機的代表作之一,它采用解釋器與即時編譯器并存的架構,
- 在Java虛擬機運行時,解釋器和即時編譯器能夠相互協作,各自取長補短,盡力去選擇最合適的方式來權衡編譯本地代碼的時間和直接解釋執行代碼的時間,
- 在今天,Java程式的運行性能早已脫胎換骨,已經達到了可以和C/C++ 程式一較高下的地步,
那為什么 我們有了更先進的 JIT編譯器 ,還要保留解釋器呢?
- 比如JRockit VM內部就不包含解釋器,位元組碼全部都依靠即時編譯器編譯后執行, JRockit虛擬機是砍掉了解釋器,也就是只采及時編譯器,那是因為呢JRockit只部署在服務器上,一般已經有時間讓他進行指令編譯的程序了,對于回應來說要求不高,等及時編譯器的編譯完成后,就會提供更好的性能
- 當程式啟動后,解釋器可以馬上發揮作用,回應速度快,省去編譯的時間,立即執行,編譯器要想發揮作用,把代碼編譯成本地代碼,需要一定的執行時間,但編譯為本地代碼后,執行效率高,
- 盡管JRockit VM中程式的執行性能會非常高效,但程式在啟動時必然需要花費更長的時間來進行編譯,對于服務端應用來說,啟動時間并非是關注重點,但對于那些看中啟動時間的應用場景而言,或許就需要采用解釋器與即時編譯器并存的架構來換取一個平衡點,
- 當解釋器與即時編譯器共存,在Java虛擬器啟動時,解釋器可以首先發揮作用,而不必等待即時編譯器全部編譯完成后再執行,這樣可以省去許多不必要的編譯時間,隨著時間的推移,編譯器發揮作用,把越來越多的代碼編譯成本地代碼,獲得更高的執行效率,
- 同時,解釋執行在編譯器進行激進優化不成立的時候,作為編譯器的“逃生門”,
總結:
- 當虛擬機啟動的時候,解釋器可以首先發揮作用,而不必等待即時編譯器全部編譯完成再執行,這樣可以省去許多不必要的編譯時間,
- 隨著程式運行時間的推移,即時編譯器逐漸發揮作用,根據熱點探測功能,將有價值的位元組碼編譯為本地機器指令,以換取更高的程式執行效率,
即時編譯器的案例
- 注意解釋執行與編譯執行在線上環境微妙的辯證關系,機器在熱機狀態可以承受的負載要大于冷機狀態(熱機已經將大量代碼編譯快取),如果以熱機狀態時的流量進行切流,可能使處于冷機狀態的服務器因無法承載流量而假死,
- 在生產環境發布程序中,以分批的方式進行發布,根據機器數量劃分成多個批次,每個批次的機器數至多占到整個集群的1/8,曾經有這樣的故障案例:某程式員在發布平臺進行分批發布,在輸入發布總批數時,誤填寫成分為兩批發布(停了一半的服務,壓力全部都壓到了新升級的冷機上),如果是熱機狀態,在正常情況下一半的機器可以勉強承載流量,但由于剛啟動的JVM均是解釋執行,還沒有進行熱點代碼統計和JIT動態編譯,導致機器啟動之后,當前1/2發布成功的服務器馬上全部宕機,此故障說明了JIT的存在,—阿里團隊
4.2 調整 執行引擎的編譯模式
默認情況下HotSpot VM是采用解釋器與即時編譯器并存的架構,當然開發人員可以根據具體的應用場景,通過命令顯式地為Java虛擬機指定在運行時到底是完全采用解釋器執行,還是完全采用即時編譯器執行,如下所示:
- -Xint:完全采用解釋器模式執行程式;
- -Xcomp:完全采用即時編譯器模式執行程式,如果即時編譯出現問題,解釋器會介入執行
- -Xmixed:采用解釋器+即時編譯器的混合模式共同執行程式,
命令方式切換:

使用下面的代碼,執行同一段代碼100W次, 各個模式的耗時情況
/**
* 測驗解釋器模式和JIT編譯模式
* -Xint : 5660ms
* -Xcomp : 781ms
* -Xmixed : 819ms
*/
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);
}
}
}
}
4.3 編譯器的相關概念
JIT 編譯器 是將代碼編譯為機器指令, 但是在java 整個體系里面,還有一個 將 java代碼轉為class位元組碼的動作,也稱為編譯,
Java 語言的“編譯期”其實是一段“不確定”的操作程序
- 因為它可能是指一個前端編譯器(編譯器的前端)把.java檔案轉變成.class檔案的程序
- 也可能是指虛擬機的后端運行期編譯器(JIT編譯器,Just In Time Compiler,編譯動作的后半部分)把位元組碼轉變成機器碼的程序,
- 還可能是指使用靜態提前編譯器(AOT編譯器,Ahead of Time Compiler)直接把.java檔案編譯成本地機器代碼的程序,
典型的編譯器:
- 前端編譯器:Sun的javac、Eclipse JDT中的增量式編譯器(ECJ),
- JIT編譯器:HotSpot VM的C1、C2編譯器,
- AOT 編譯器:GNU Compiler for the Java(GCJ)、Excelsior JET,
4.4熱點代碼
是否需要啟動JIT編譯器將位元組碼直接編譯為對應平臺的本地機器指令,是需要根據代碼被呼叫執行的頻率而定,
-
關于那些需要被編譯為本地代碼的位元組碼,比如經常被執行的方法,或者一個方法內部回圈次數較多的回圈體都可被稱之為“熱點代碼”,JIT編譯器在運行時會針對那些頻繁被呼叫的“熱點代碼”做出深度優化,將其直接編譯為對應平臺的本地機器指令,以此提升Java程式的執行性能,由于這種編譯方式都是發生在方法執行的程序中, 因此也被稱為
堆疊上替換, 所以JIT編譯也可以稱為 OSR(On Stack Replacement) 編譯 -
一個方法究竟要被呼叫多少次,或者一個回圈體究竟需要執行多少次回圈才可以達到這個標準?
必然需要一個明確的閾值,JIT編譯器才會將這些“熱點代碼”編譯為本地機器指令執行,這里主要依靠熱點探測功能,
熱點探測技術
目前HotSpot VM所采用的熱點探測方式是基于計數器的熱點探測,采用基于計數器的熱點探測,HotSpot VM將會為每一個方法都建立2個不同型別的計數器,分別為方法呼叫計數器(Invocation Counter)和回邊計數器(Back Edge Counter),
- 方法呼叫計數器用于統計方法的呼叫次數
- 回邊計數器則用于統計回圈體執行的回圈次數
4.5方法呼叫計數器
這個計數器就用于統計方法被呼叫的次數,它的默認閥值在Client模式下是1500(兩個計數器之和)次,在Server模式下是10000(兩個計數器之和)次,超過這個閾值,就會觸發JIT編譯,
這個閥值可以通過虛擬機引數 -XX:CompileThreshold 來人為設定,
當一個方法被呼叫時,會先檢查該方法是否存在被JIT編譯過的版本
- 如果存在,則優先使用編譯后的本地代碼來執行
- 如果不存在已被編譯過的版本,則將此方法的呼叫計數器值加1,然后判斷方法呼叫計數器與回邊計數器值之和是否超過方法呼叫計數器的閥值,
- 如果已超過閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求,再執行代碼
- 如果未超過閾值,則使用解釋器對位元組碼檔案解釋執行
示意圖:

但是這里有一個問題, 如果jvm 運行時間足夠長,理論上所有的代碼都會達到閾值,被JIT編譯,這顯然是不合理的,所以 ,我們還需要熱度衰減的程序
-
如果不做任何設定,方法呼叫計數器統計的并不是方法被呼叫的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被呼叫的次數,
-
當超過一定的時間限度,如果方法的呼叫次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的呼叫計數器就會被減少一半,這個程序稱為方法呼叫計數器熱度的衰減(Counter Decay),而這段時間就稱為此方法統計的半衰周期(Counter Half Life Time)
-
JVM 進行熱度衰減的動作是在虛擬機進行垃圾收集時順便進行的,可以使用虛擬機引數 ,
-XX:-UseCounterDecay來關閉熱度衰減 ,也可以使用-XX:CounterHalfLifeTime引數設定半衰周期的時間,單位是秒, -
如果關閉熱度衰減, 讓方法計數器統計方法呼叫的絕對次數,只要系統運行時間足夠長,絕大部分方法都會被編譯成本地代碼,
4.6 回邊計數器
它的作用是統計一個方法中回圈體代碼執行的次數,在位元組碼中遇到控制流向后跳轉的指令(也就是回圈的指令)稱為“回邊”(Back Edge),顯然,建立回邊計數器統計的目的就是為了觸發OSR編譯(JIT編譯),
示意圖:

4.7 Hotspot JIT編譯器的分類
JIT的編譯器還分為了兩種,分別是C1和C2,在HotSpot VM中內嵌有兩個JIT編譯器,分別為Client Compiler和Server Compiler,但大多數情況下我們簡稱為C1編譯器 和 C2編譯器,開發人員可以通過如下命令顯式指定Java虛擬機在運行時到底使用哪一種即時編譯器,
除了都可以將位元組碼指令編譯為機器碼快取,加快運行速度外, 他們在編譯程序中,還有不同程度的優化.如下所示:
-
-client:指定Java虛擬機運行在Client模式下,并使用C1編譯器;
C1編譯器會對位元組碼進行簡單和可靠的優化,耗時短,以達到更快的編譯速度,
-
-server:指定Java虛擬機運行在server模式下,并使用C2編譯器, (windows 64 默認jdk模式)
C2進行耗時較長的優化,以及激進優化,但優化的代碼執行效率更高,(使用C++)
C1 和 C2編譯器不同的優化策略
C1編譯器上主要有方法行內,去虛擬化、冗余消除,
- 方法行內:將參考的函式代碼編譯到參考點處,這樣可以減少堆疊幀的生成,減少引數傳遞以及跳轉程序
- 去虛擬化:對唯一的實作方法進行行內
- 冗余消除:在運行期間把一些不會執行的代碼折疊掉
C2的優化主要是在全域層面,逃逸分析是優化的基礎,基于逃逸分析在C2上有如下幾種優化
- 標量替換:用標量值代替聚合物件的屬性值
- 堆疊上分配:對于未逃逸的物件分配物件在堆疊而不是堆
- 同步消除:清除同步操作,通常指synchronized
但是Hotspot 中又有分層編譯策略的模式,所以上面的所有優化策略,都有可能被使用到
- 分層編譯(Tiered Compilation)策略:程式編譯執行可以觸發C1編譯(不開啟性能監控),將位元組碼編譯成機器碼,可以進行簡單優化,也可以加上性能監控,C2編譯會根據性能監控資訊進行激進優化,
- 不過在Java7版本之后,一旦開發人員在程式中顯式指定命令“-server"時,默認將會開啟分層編譯策略,由C1編譯器和C2編譯器相互協作共同來執行編譯任務,
總結:
- 一般來講,JIT編譯出來的機器碼性能比解釋器解釋執行的性能高
- C2編譯器啟動時長比C1慢,系統穩定執行以后,C2編譯器執行速度遠快于C1編譯器 (磨刀不誤砍柴工)
5. 未來可期的新技術
Graal 編譯器
-
自JDK10起,除了C1和C2編譯器外, HotSpot又在JIT編譯器加入了一個全新的及時編譯器:Graal編譯器
-
編譯效果短短幾年時間就追平了G2編譯器
-
目前,還帶著實驗狀態標簽,需要使用開關引數去激活才能使用
-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler
AOT 編譯器
- 除了JIT即時編譯器,jdk9又引入了AoT編譯器(靜態提前編譯器,Ahead of Time Compiler)
- Java 9引入了實驗性AOT編譯工具jaotc,它借助了Graal編譯器,將所輸入的Java類檔案轉換為機器碼,并存放至生成的動態共享庫之中,
- 所謂AOT編譯,是與即時編譯相對立的一個概念,
- 即時編譯指的是在程式的運行程序中,將位元組碼轉換為可在硬體上直接運行的機器碼,并部署至托管環境中的程序,
- 而AOT編譯指的則是,在程式運行之前,就根據不同的平臺,將位元組碼轉換為對應機器碼的程序,
.java -javac-> .class -jaotc-> .so(機器碼)
優點:
- Java虛擬機加載已經預編譯成二進制庫,可以直接執行,
- 不必等待即時編譯器的預熱,減少Java應用給人帶來“第一次運行慢” 的不良體驗
缺點:
- 破壞了 java “ 一次編譯,到處運行”,必須為每個不同的硬體,OS編譯對應的發行包
- 降低了Java鏈接程序的動態性,加載的代碼在編譯器就必須全部已知,
- 還需要繼續優化中,最初只支持Linux X64 java base
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/231282.html
標籤:Java
上一篇:Spring學習02
下一篇:萌新求助,資料約定是什么意思??
