執行引擎
- 執行引擎是Java虛擬機核心組成部分之一
- 虛擬機是一個相對于物理機的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、緩存、指令集和操作系統層面上的,而虛擬機的執行引擎是由軟件自行實現的,因此可以不受物理條件制約地定制指令集與執行引擎的結構體系,能夠執行那些不被硬件直接支持的指令集格式
- JVM主要任務負責裝載字節碼到其內部,但字節碼并不能直接運行在操作系統上,因為字節碼指令并非等價于本地機器指令,它內部包含的僅只是一些能夠被JVM所識別的字節碼指令、符號表、以及其他輔助信息。
- 如果一個Java程序運行起來,執行引擎的任務就是將字節碼指令解釋/編譯為對應平臺上的本地機器指令才可以,JVM的執行引擎充當了將高級語言翻譯為機器語言的翻譯者
執行引擎工作過程
- 執行引擎在執行的過程中究竟需要執行什么樣的字節碼指令完全依賴于PC寄存器
- 每當執行完一項指令操作后,PC寄存器就會更新下一條需要執行的指令地址
- 當方法在執行的過程中,執行引擎有可能會通過存儲在局部變量表中的對象引用準備定位到存儲在Java堆區中的對象實例信息,以及通過對象頭中的元數據指針定位目標對象的類型信息
Java代碼編譯和執行
-
大部分的程序代碼轉換為物理機的目標代碼或虛擬機能執行的指令集之前,都需要經過下圖各個步驟
-
Java代碼編譯是由Java源碼編譯器來完成,流程圖如下
-
Java字節碼的執行是由JVM執行引擎來完成
-
什么是解釋器(Interpreter),什么是JIT編譯器
- 解釋器:當Java虛擬機啟動時會根據預定義的規范對字節碼采用逐行解釋的方式執行,將每條字節碼文件中的內容翻譯為對應平臺的本地機器指令執行
- JIT(Just In Time Compiler)編譯器:虛擬機將源代碼直接編譯成和本地機器平臺相關的機器語言
機器碼
- 各種用于進制編碼方式表示的指令,叫機器指令碼
- 機器語言雖然被計算機理解和接受,但和人們的語言差別太大,不易被人們理解和記憶,并且編程容易出錯
- 用它編寫的程序一經輸入計算機,CPU直接讀取執行,執行速度更快
- 機器指令與CPU緊密相關,所以不同種類CPU所對應的機器指令不同
指令
- 由于機器碼有0和1組成二進制序列,可讀性差,人們發明了指令
- 指令就是把機器中的0和1序列,簡化成對應的指令,可讀性稍好
- 由于不同硬件平臺,執行同一個操作,對應的機器碼可能不同,所以不同硬件平臺的同一種指令,對應機器碼不同
指令集
- 不同硬件平臺,各自支持的指令,是有差別的,每個平臺所支持的指令稱之為對應平臺的指令集
- x86指令集,對應的是x86架構的平臺
- ARM指令集,對應的是Arm架構的平臺
匯編語言
- 由于指令的可讀性還是太差,于是人們發明了匯編語言
- 在匯編語言中,用助記符代替機器指令的操作碼,用地址符號或標號代替指令或操作數地址
- 在不同硬件平臺,匯編語言對應著不同的機器語言指令集,通過匯編過程轉換成機器語言
- 由于計算機只認識指令碼所以匯編還必須翻譯成機器指令碼,計算機才能識別和執行
高級語言
- 為了使計算機用戶編寫程序更容易,后來出現了更接近人類的高級語言
- 當計算機執行高級語言編寫的程序時,仍然需要把程序解釋和編譯成機器的指令碼,完成這個過程的程序叫解釋程序或編譯程序
字節碼
- 字節碼是一種中間狀態的二進制代碼,它比機器碼更抽象,需要直譯器轉譯后才能成為機器碼
- 字節碼主要為了實現特定軟件運行和軟件環境、硬件環境無關
- 字節碼實現方式是通過編譯器和虛擬機器,編譯器將源碼編譯成字節碼
- 字節碼的典型應用為Java bytecode
- 字節碼的典型應用為Java bytecode
解釋器
- JVM設計進初衷只是單純為了滿足Java程序實現跨平臺特性,因此避免采用靜態編譯的方式直接生成本地機器指令,從而誕生了實現解釋器在運行時采用逐行解釋字節碼執行程序的想法
- 解釋器真正意義上所承擔的角色就是一個運行時“翻譯者”,將字節碼文件中的內容“翻譯”為對應平臺的本地機器指令執行
- 當一條字節碼指令被解釋執行完成后,接著再根據PC寄存器中記錄的下一條需要被執行的字節碼指令執行解釋操作
- 在Java發展歷史中,一共有兩套解釋器,一種為古老的字節碼解釋器,另一種為目前使用的模板解釋器
- 字節碼解釋器在執行時通過純軟件代碼模氛字節碼的執行,效率非常低下
- 模板解釋字將每一條字節碼和一個模板函數相關聯,模板函數直接產生這條字節碼執行時的機器碼,從而很大程度上提高了解釋器的性能
- 在Hotspot VM中,解釋器由Interpreter模塊和Code模塊構成。
- Intercepter實現了解釋器的核心功能
- Code模塊用于管理Hotspot VM在運行時生成的本地機器指令
- 在Hotspot VM中,解釋器由Interpreter模塊和Code模塊構成。
JIT編譯器
- 前端編譯器
- Sun的Javac
- Eclipse JDT中的增量式編譯器(ECJ)
- 后端編譯器
- JIT編譯器(后端運行期編譯器)
- Hotspot VM的C1、C2編譯器
- AOT編譯器(靜態提前編譯器)
- GNU Compiler for the Java(GCJ)
- Excelsior JET
- JIT編譯器(后端運行期編譯器)
有了JIT,為什么還需要解釋器
- 當程序啟動后,解釋器可以馬上發揮作用,省去編譯時間,立即執行
- 編譯器要想發揮作用,把代碼編譯成本地代碼,需要一定的執行時間,但編譯為本地代碼后,執行效率高
- 對于服務器端應用,啟動時間并非是關注重點,但對于看中啟動時間的應用場景而言,采用解釋器與即時編譯器共存的架構換取一個平衡點,Java虛擬機啟動時,解釋器可以首先發揮作用,而不必等待即時編譯器全部編譯完成后再執行,這樣可以省去許多不必要的編譯時間,隨著時間推移,編譯器發揮作用,根據熱點探測功能,把越來越多的代碼編譯成本地代碼,獲得更高的執行效率
- 解釋執行在編譯激進優化不成立時,作為編譯器的“逃生門”
熱點代碼及探測方式
當然是否需要啟動JIT編譯器將字節碼直接編譯為對應平臺的本地機器指令,則需要根據代碼被調用執行的頻率而定,關于那些需要被編譯為本地代碼的字節碼,稱為“熱點代碼”,JIT編譯器在運行時會針對那些頻繁被調用的熱點代碼做出深度優化,將其直接編譯為對應平臺的本地機器指令,以此提升Java程序的執行性能。
- 一個被多次調用的方法,或一個方法體內部循環次數較多的循環體都可以被稱為熱點代碼,因此可以通過JIT編譯為本地機器指令,由于這種編譯方式發生在方法的執行過程中,因此稱為棧上替換或OSR(On Stack Replacement)編譯
- 一個方法究竟要被調用多少次,或者循環體究竟需要執行多少次循環才可以達到這個標準,需要一個明確的閾值,依靠熱點探測功能
- 目前Hotspot采用基于計數器的熱點探測
- 采用計數器的熱點探測,Hotspot VM將為每個方法建立2個不同類型的計數器,分別為方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)
- 方法調用計數器用于統計方法的調用次數
- 默認閾值在Client模式下1500次,在Server模式下10000次,超過這個閾值,會觸發JIT編譯
- 閾值可能通過虛擬機參數-XX:CompileThreshold來人為設定
- 當一個方法被調用時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優先使用編譯后的本地代碼來執行,不存在,則將此方法的調用計數器加1,然后判斷方法調用計數器與回邊計數器之各是否超過方法閾值。如果超過,那么將會向即時編譯器提交一個該方法的代碼編譯請求
- 熱度衰減
- 如果不做任何設置,方法調用計數器統計的并不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被調用的次數。當超過一定的時間限制,如果方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減少一半,這個過程稱為方法調用計數器熱度的衰減(Counter Decay),而這段時間稱為方法統計的半衰周期(Counter Half Life Time)
- 進行熱度衰減的動作是在虛擬機進行垃圾收集時順便進行的,可以使用虛擬機參數-XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法調用的絕對次數,這樣,只要系統運行時間足夠長,絕大部分方法都會被編譯成本地代碼
- 另外,可以使用-XX:CounterHalfLifeTime參數設置半衰周期的時間,單位秒
- 回邊計數器用于統計循環體執行的循環次數
- 在字節碼中遇到控制流向后跳轉的指令稱為“回邊”,建立回邊計數器統計的目的是為了觸發OSR編譯
- 在字節碼中遇到控制流向后跳轉的指令稱為“回邊”,建立回邊計數器統計的目的是為了觸發OSR編譯
- 方法調用計數器用于統計方法的調用次數
修改VM編譯模式
- -Xint:完全采用解釋器模式執行程序
- -XComp:完全采用即時編譯器模式,如即時編譯出現問題,解釋器會介入執行
- -Xmixed:采用解釋器和即時編譯器混合模式共同執行程序(默認)
/*** 測試三種模式* -Xint:花費時間:4453* -Xcomp:花費時間:594* -Xmixed:花費時間:672*/
public class IntCompTest {public static void main(String[] args) {long start = System.currentTimeMillis();testPrimeNumber(1000000);long end = System.currentTimeMillis();System.out.println("花費時間:" + (end - start));}private static void testPrimeNumber(int counter) {for (int i = 0; i < counter; 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;}}}}}
}
Hotspot JIT分類
- Client Compiler
-
- client:指定Java虛擬機運行在Client模式下,并使用C1編譯器
- C1編譯器會對字節碼進行簡單和可靠的優化,耗時短,以達到更快的編譯速度
- 優化策略
- 方法內聯:將引用的函數代碼編譯到引用點處,這樣可以減少棧幀的生成,減少參數傳遞以及跳轉過程
- 去虛擬化:對唯一的實現類進行內聯
- 冗余消除:在運行期間把一些不會執行的代碼折疊掉
-
- Server Compiler
- -server:指定Java虛擬機運行在Server模式下,并使用C2編譯器
- C2進行耗時較長的優化,以及激進優化,但優化代碼執行效率更高
- 優化策略,在全局層面,逃逸分析是優化基礎
- 標量替換:用于標量值代替聚合對象的屬性值
- 線上分配:對于未逃逸的對象分配對象在棧而不在堆
- 同步消除:消除同步操作,通常指sychronized
- -server:指定Java虛擬機運行在Server模式下,并使用C2編譯器