本篇文章圍繞執行引擎,深入淺出的解析執行引擎中直譯器與編譯器的解釋執行和編譯執行、執行引擎的執行方式、逃逸分析帶來的棧上分配、鎖消除、標量替換等最佳化以及即時編譯器編譯對熱點程式碼的探測
執行引擎
hotspot執行引擎結構圖
執行引擎分為直譯器、JIT即時編譯器以及垃圾收集器
執行引擎透過直譯器/即時編譯器將位元組碼指令解釋/編譯為對應OS上的的機器指令
本篇文章主要圍繞直譯器與即時編譯器,垃圾收集器將在後續文章解析
解釋執行與編譯執行
Java虛擬機器執行引擎在執行Java程式碼時,會有兩種選擇:解釋執行和編譯執行
解釋執行:透過位元組碼直譯器把位元組碼解析為機器語言執行
編譯執行:透過即時編譯器產生原生程式碼執行
Class檔案中的程式碼到底是解釋執行還是編譯執行只有Java虛擬機器自己才能判斷準確
編譯過程
編譯流程在前一篇文章深入淺出JVM之前端編譯過程與語法糖原理已經說明,在本篇文章中不再概述
經典編譯原理: 1.對原始碼進行詞法,語法分析處理 2.把原始碼轉換為抽象語法樹
javac編譯器完成了對原始碼進行詞法,語法分析處理為抽象語法樹,再遍歷抽象語法樹生成線性位元組碼指令流的過程
剩下的指令流有兩種方式執行
- 由虛擬機器內部的位元組碼直譯器去將位元組碼指令進行逐行解釋 (解釋執行)
- 或最佳化器(即時編譯器)最佳化程式碼最後生成目的碼 (編譯執行)
執行引擎流程圖
直譯器與編譯器
直譯器
作用: 對位元組碼指令逐行解釋
優點: 程式啟動,直譯器立即解釋執行
缺點: 低效
即時編譯器 (just in time compiler)
Java中的"編譯期"不確定
- 可能說的是執行javac指令時的前端編譯器 (.java->.class)
- 也可能是後端編譯器JIT (位元組指令->機器指令)
- 還可能是AOT編譯器(靜態提前編譯器) (.java->機器指令)
作用: 將方法編譯成機器碼快取到方法區,每次呼叫該方法執行編譯後的機器碼
優點: 即時編譯器把程式碼編譯成本地機器碼,執行效率高,高效
缺點: 程式啟動時,需要先編譯再執行
執行引擎執行方式
執行引擎執行方式大致分為3種
-Xint
: 完全採用直譯器執行
-Xcomp
: 優先採用即時編譯器執行,直譯器是後備選擇
-Xmixed
: 採用直譯器 + 即時編譯器
hotspot中有兩種JIT即時編譯器
Client模式下的C1編譯器:簡單最佳化,耗時短(C1最佳化策略:方法內聯,去虛擬化,冗餘消除)
Server模式下的C2編譯器:深度最佳化,耗時長 (C2主要是逃逸分析的最佳化:標量替換,鎖消除,棧上分配)
分層編譯策略:程式解釋執行(不開啟逃逸分析)可以觸發C1編譯,開啟逃逸分析可以觸發C2編譯
直譯器,C1,C2編譯器同時工作,熱點程式碼可能被編譯多次
直譯器在程式剛剛開始的時候解釋執行,不需要承擔監控的開銷
C1有著更快的編譯速度,能為C2編譯最佳化爭取更多時間
C2用高複雜度演算法,編譯最佳化程度很高的程式碼
逃逸分析帶來的最佳化
當物件作用域只在某個方法時,不會被外界呼叫到,那麼這個物件就不會發生逃逸
開啟逃逸分析後,會分析物件是否發生逃逸,當不能發生逃逸時會進行棧上分配、鎖消除、標量替換等最佳化
棧上分配記憶體
//-Xms1G -Xmx1G -XX:+PrintGCDetails
public class StackMemory {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
memory();
}
System.out.println("花費時間:"+(System.currentTimeMillis()-start)+"ms");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void memory(){
StackMemory memory = new StackMemory();
}
}
-XX:-DoEscapeAnalysis 花費時間:63ms (未開啟逃逸分析)
-XX:+DoEscapeAnalysis 花費時間:4ms (開啟逃逸分析)
預設開啟逃逸分析
鎖消除
同步加鎖會帶來開銷
鎖消除:當加鎖物件只作用某個方法時,JIT編譯器藉助逃逸分析判斷使用的鎖物件是不是隻能被一個執行緒訪問,如果是這種情況下就不需要同步,可以取消這部分程式碼的同步,提高併發效能
標量替換
標量: 無法再分解的資料 (基本資料型別)
聚合量: 還可以再分解的資料 (物件)
標量替換: JIT藉助逃逸分析,該物件不發生逃逸,只作用於某個方法會把該物件(聚合量)拆成若干個成員變數(標量)來代替
預設開啟標量替換
public class ScalarSubstitution {
static class Man{
int age;
int id;
public Man() {
}
}
public static void createInstance(){
Man man = new Man();
man.id = 123;
man.age = 321;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
createInstance();
}
System.out.println("花費時間:"+(System.currentTimeMillis()-start)+"ms");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//-Xmx200m -Xms200m -XX:+PrintGCDetails
//-XX:+DoEscapeAnalysis 設定開啟逃逸分析
//-XX:-EliminateAllocations 設定不開啟標量替換
//開啟逃逸分析 + 關閉標量替換 : 花費時間:93ms
//開啟逃逸分析 + 開啟標量替換 : 花費時間:6ms
熱點程式碼與熱點探測
JIT編譯器並不是編譯所有的位元組碼,JIT編譯器只編譯熱點程式碼
熱點程式碼: 被多次呼叫的方法 或 方法中多次迴圈的迴圈體
棧上替換(OSR): JIT將方法中的熱點程式碼編譯為本地機器指令(被多次執行的迴圈體)
編譯物件都是方法,如果是棧上替換則"入口"在方法的迴圈體開始那裡
熱點探測功能決定了被呼叫多少次的方法能成為熱點程式碼
hotspot採用基於計數器的熱點探測
- 方法呼叫計數器 : 統計方法呼叫次數
- 回邊計數器 : 統計迴圈體執行迴圈次數
方法呼叫時先判斷是否有執行編譯後的機器碼,有則直接使用方法區的Code cache中的機器碼;沒有機器碼則判斷計數器次數是否超過閾值,超過則觸發編譯,編譯後機器碼儲存在方法區Code cache中使用;最後都沒有就使用解釋執行
總結
本篇文章將圍繞執行引擎,深入淺出的解析執行引擎中的直譯器、即時編譯器各自執行的優缺點以及原理
執行引擎由直譯器、即時編譯器、垃圾收集器構成,預設情況下使用直譯器與編譯器的混合方式執行
即時編譯器分為C1、C2編譯器,其中C1編譯快但最佳化小,C2開啟逃逸分析使用棧上分配、鎖消除、標量替換進行最佳化,編譯耗時但是最佳化大
即時編譯器並不是所有程式碼都編譯,而是使用方法技術和迴圈計數來將熱點程式碼編譯成機器碼存放在方法區的Code Cache中
在混合執行的模式下,直譯器、C1、C2編譯器同時工作,分層編譯
最後(一鍵三連求求拉~)
本篇文章筆記以及案例被收入 gitee-StudyJava、 github-StudyJava 感興趣的同學可以stat下持續關注喔\~
有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關注、收藏支援一下\~
關注菜菜,分享更多幹貨,公眾號:菜菜的後端私房菜
本文由部落格一文多發平臺 OpenWrite 釋出!