1. 程式人生 > >Java編譯(三) Java即時編譯(JIT編譯):執行時把Class檔案位元組碼編譯成本地機器碼

Java編譯(三) Java即時編譯(JIT編譯):執行時把Class檔案位元組碼編譯成本地機器碼

Java編譯(三)Java即時編譯(JIT編譯):

執行時把Class檔案位元組碼編譯成本地機器碼

下面我們詳細瞭解JIT編譯;從官方JDK中的HotSpot虛擬機器的JIT編譯器入手,先介紹直譯器與JIT編譯器是如何配合工作的,認識JIT編譯器C1/C2;再看看JIT編譯的是什麼,以及觸發條件是什麼;而後再簡單介紹JIT編譯過程,認識幾種編譯技術;最後對比Java與C/C++的編譯器。

1、直譯器與JIT編譯器

即時編譯功能與虛擬機器具體實現有關,下文的編譯器、即時編譯器指HotSpot虛擬機器內的即時編譯器。

1-1、直譯器與JIT編譯器的結合方式

前面文章介紹過HotSpot是採用前端編譯+JIT編譯的方式,所以需要直譯器來解釋執行載入進來的Class位元組碼,直譯器與JIT編譯器的結合方式(即後面說明的混合模式)如下

1、直譯器

程式啟動時首先發揮作用,解釋執行Class位元組碼;

省去編譯時間,加快啟動速度;

但執行效率較低;

2、JIT編譯器

程式解釋執行後,JIT編譯器逐漸發揮作用

編譯成原生代碼,提高執行效率;    

但佔用程式執行時間、記憶體等資源;

3、激進優化的"逃生門"

直譯器還可以作JIT編譯器激進優化的一個"逃生門";

激進優化不成立時,可以通過逆優化(Deoptimization)退回到解釋狀態編譯執行

1-2、JIT編譯器:Client Compiler與Server Comiler

HotSpot虛擬機器內建兩個即時編譯器,分別Client Compiler和Server Comiler,如下:

1、Client Compiler

簡稱C1編譯器

(A)、應用特點

較為輕量,只做少量效能開銷比較高的優化,它佔用記憶體較少,適合於桌面互動式應用;

(B)、優化技術

它是一個簡單快速的三段式編譯器;

主要關注點在於區域性性的優化,而放棄了許多耗時較長的全域性優化;

在暫存器分配策略上,JDK6以後採用的為線性掃描暫存器分配演算法,其他方面的優化,主要有方法內聯、去虛擬化、冗餘消除等;

(C)、設定引數

可以使用"-client"引數強制選擇執行在Client模式(Client VM);

2、Server Compiler

簡稱C2編譯器,也叫Opto編譯器;

(A)、應用特點

較為重量,採用了大量傳統編譯優化的技巧來進行優化,佔用記憶體相對多一些,適合伺服器端的應用;

(B)、優化技術

它會執行所有經典的優化動作,如無用程式碼消除、迴圈展開、迴圈表示式外提、消除公表示式、常量傳播、基本塊重排序等;

還會一些與Java語言特性密切相關的優化技術,如範圍檢查消除、空值檢查消除等;

另外,還進行一些不穩定的激進優化,如守護內聯、分支頻率預測等;

(C)、收集效能資訊

由於C2會收集程式執行資訊,因此其優化範圍更多在於全域性優化,不僅僅是一個方塊的優化;

收集的資訊主要有:分支的跳轉/不跳轉的頻率、某條指令上出現過的型別、是否出現過空值、是否出現過異常等。

(D)、與C1的不同點

和C1的不同主要在於暫存器分配策略及優化範圍,暫存器分配策略上C2採用傳統的全域性圖著色暫存器分配演算法;

C2編譯速度較為緩慢,但遠遠超過傳統的靜態優化編譯器;

而且編譯輸出的程式碼質量高,可以減少原生代碼的執行時間;

(E)、設定引數

可以使用"-server"引數強制選擇執行在Server模式(Server VM);

1-3、JIT編譯器與直譯器的工作模式

可以通過“-version”引數顯示當前的工作模式,各工作模式說明如下:

1、混合模式(Mixed Mode)

JIT編譯器(無論C1還是C2)與直譯器配合工作的方式;

這是預設的方式,也可通過“-Xmixed”引數設定;

2、解釋模式(Interpreted Mode)

全部程式碼由直譯器解釋執行,JIT編譯器不介入工作;

可以通過“-Xint”引數設定;

3、編譯模式(Compiler Mode)

優先採用編譯方式執行程式,但直譯器仍要在編譯無法時行時介入執行過程;

可以通過“-Xcomp”引數設定;

該引數強調的是首次呼叫方法時執行編譯,並不是不用直譯器

一般情況下(不開啟分層編譯),一個方法需要解釋執行一定次數後才編譯(詳見後面“熱點探測“);

但JDK7/8作為預設開啟分層編譯策略

1-4、分層編譯

為了在程式啟動響應速度與執行效率之間達到最佳平衡,會啟用分層編譯(Tiered Compilation)策略;

1、編譯層次

根據編譯器編譯、優化的規模與耗時,劃分出不同的編譯層次,包括:

(I)、第0層

程式解釋執行,直譯器不開啟效能監控功能(Profiling),可觸發第1層編譯;

(II)、第1層

也稱為C1編譯,將位元組碼編譯為原生代碼,進行簡單、可靠的優化,如有必要加入效能監控的邏輯;

(III)、第2層

也稱為C2編譯,也是將位元組碼編譯為原生代碼,但進行一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化;

2、優點

這時C1和C2同時進行工作,許多程式碼都可能被編譯多次;

用C1獲取更高的編譯速度,用C2獲取更好的編譯質量;

直譯器執行時也無須再承擔收集效能監控資訊的任務(如果不開啟分層編譯,又工作在Server模式,直譯器提供監控資訊給C2使用);

最終在程式啟動響應速度與執行效率之間達到最佳平衡;

3、設定引數

JDK6開始出現,需要“-XX:+TieredCompilation”指定開啟;

JDK7/8作為預設的策略,可以通過“-XX:-TieredCompilation”關閉策略

注意,只能在Server模式下使用;

4、原始碼分析

原始碼中對“TieredCompilation”引數的解析過程,如下:

可以看到設定了"AdvancedThresholdPolicy"物件作為分層編譯策略的實現,"AdvancedThresholdPolicy.cpp"中有關於分層編譯的更詳細的說明,如下:

可以看到它裡面更詳細的分為了5層(上面的第1層C1編譯包括了裡面的1、2、3層);

另外,不同層有一些不同引數可以設定,如下:

2、JIT編譯物件與觸發條件

2-1、熱點程式碼

JIT編譯物件為"熱點程式碼",包括兩類:

1、被多次呼叫的方法

由方法呼叫觸發的編譯,以整個方法體為編譯物件;

JVM中標準的的JIT編譯方式

2、被多次執行的迴圈體

由迴圈體觸發,仍然以整個個方法體為編譯物件;

發生在方法執行過程中,方法棧幀還在棧上,方法就被替換;

稱為棧上替換(On Stack Replacement),簡稱OSR編譯

2-2、熱點探測(Hot Spot Deection)方法

判斷一段程式碼是不是熱點程式碼,是不是需要編譯,目前主要有兩種方法:

1、基於取樣的熱點探測(Sample Base Hot Spot Detection)

JVM週期性檢查各個執行緒的棧頂,看某些方法是否經常出現在棧頂;

優點:簡單高效,且容易獲得方法呼叫關係;

缺點:不精確,容易受到執行緒阻塞或外界因素影響;

2、基於計數器的熱點探測(Counter Base Hot Spot Detection)

為每方法(程式碼塊)建立計數器,統計執行次數,超過一定閾值就認為是"熱點程式碼";

優點:更加精確和嚴謹;

缺點:比較複雜;                

當然還有其他的方法,如Android Dalvik中的JIT編譯器使用的基於"蹤跡"(Trace)的熱點探測;

HotSpot虛擬機器使用第二種--基於計數器的熱點探測。

2-3、基於計數器的熱點探測

注意,下面的互動過程都是以Client模式JIT編譯為例子說明,如果Server模式比較複雜一些,而啟用分層編譯模式則更復雜。

HotSpot虛擬機器為每個方法準備了兩類計數器,來統計執行次數,如下:

1、方法呼叫計數器(Invocation Counter)

(1)、互動過程

Client模式時的互動過程,如圖:

(A)、當方法執行,先檢查是否存在被JIT編譯的版本

如果存在,則用編譯的原生代碼執行;

如果不存在,則計數器值加一;

(B)、判斷兩個計數器(加上回邊計數器)之和是否超過閾值

如果沒超過,則以解釋方式執行方法;

如果超過,則向編譯器提交編譯請求;

(C)、提交編譯請求後的執行方式

如果以預設設定,提交編譯請求後,繼續以解釋方式執行方法;

如果通過"-Xbatch "或"-XX:-BackgroundCompilation",設定成同步等待方式,則等待編譯完成,以編譯後代碼執行

(2)、閾值設定

預設C1時為1500次(sparc平臺才是1000),C2時為10000次;

可以通過"-XX:CompileThreshold"引數設定

啟用分層編譯時將忽略此選項,請參閱選項"-XX:+ TieredCompilation"

(3)、閾值表示

(A)、執行頻率

預設設定下,方法呼叫計數器不是統計呼叫的絕對次數,而是執行頻率:一段時間內方法被呼叫的次數

如果一段時間內沒達到閾值觸發編譯請求,就會在JVM進行垃圾回收時,把計數器值減半,這個過程稱為方法呼叫計數器熱度的衰減(Counter Decay),這段時間稱為方法呼叫計數的半衰週期(Counter Half Life Time);

可以通過"-XX:CounterHalfLifeTime"引數設定半衰週期的時間(秒)

(B)、絕對次數

可以通過"-XX:-UseCounterDecay"引數關閉熱度衰減

這時方法呼叫計數器統計的就是方法呼叫的絕對次數

2、回邊計數器(Back Edge Counter)

位元組碼中遇到控制流向後中轉的指令,稱為"回邊"(Back Edge)

回邊計數器作用:統計一個方法中迴圈體程式碼執行次數,為觸發OSR編譯;

(1)、互動過程

Client模式時的互動過程,如圖:

(A)、當執行中遇到回邊指令,先檢查將要執行的程式碼片段是否存在被JIT編譯的版本:

如果存在,則用編譯的原生代碼執行;

如果不存在,則計數器值加一;

(B)、判斷兩個計數器(加上方法呼叫計數器)之和是否超過閾值:

如果沒超過,則以解釋方式執行方法;

如果超過,則向編譯器提交編譯請求,調整減少回邊計數器值;

(C)、提交編譯請求後的執行方式

如果以預設設定,提交OSR編譯請求後,繼續以解釋方式執行方法;

如果通過"-Xbatch "或"-XX:-BackgroundCompilation",設定成同步等待方式,則等待編譯完成,以編譯後代碼執行

(2)、閾值設定

簡單策略下,並沒有使用"-XX:BackEdgeThreshold"引數設定閾值;

而是使用OnStackReplacePercentage,該值參與計算是否觸發OSR編譯的閾值;

可以通過"-XX:OnStackReplacePercentage"來設定,然後通過一定規則計算,如下;

(i)、Client模式                    

計算規則方法呼叫計數器閾值(CompileThreshold)*OSR比率(OnStackReplacePercentage)/100

預設:OnStackReplacePercentage=933, CompileThreshold=1500,計算閾值為14895

(ii)、Server模式

前面介紹分層編譯時曾說:如果不開啟分層編譯,又工作在Server模式,直譯器提供監控資訊給C2使用,所以多了個直譯器監控比率(InterpreterProfilePercentage);

計算規則CompileThreshold*(OnStackReplacePercentage-InterpreterProfilePercentage)/100;

預設:OnStackReplacePercentage=140, CompileThreshold=10000,InterpreterProfilePercentage=33,計算閾值為10700

可以看到HotSpot原始碼定義的計算規則,如下:

if (ProfileInterpreter) {
   //Server模式
   InterpreterBackwardBranchLimit = (CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)) / 100;
} else {
   //Client模式
   InterpreterBackwardBranchLimit = ((CompileThreshold * OnStackReplacePercentage) / 100) << number_of_noncount_bits;
}

(3)、閾值表示

統計的就是方法中迴圈體程式碼執行的絕對次數

沒有執行頻率、熱度衰減的概念

3、JIT編譯過程

下面分別對C1、C2編譯器的編譯過程進行簡單介紹。

3-1、C1編譯過程

它是一個簡單快速的三段式編譯器,主要關注點在於區域性性的優化,而放棄了許多耗時較長的全域性優化;

三段式編譯過程如下:

(A)、在位元組碼上進行一些基礎優化,如方法內聯、常量傳播等;

然後將位元組碼構造成一種高階中間程式碼表示(High-Level Intermediate Representaion,HIR);

HIR使用靜態單分配(SSA)的形式表示程式碼值;

(B)、在HIR基礎上再次進行一些優化,空值檢查消除、範圍檢查消除等;

然後將HIR轉換為LIR(低階中間程式碼表示)

(C)、在LIR基礎上分配暫存器、做窺孔優化,然後生成機器碼;

3-2、C2編譯過程

C2的編譯過程較為複雜,從前面對C2的介紹可以知道,C2編譯採用了很多優化技術,後面再對一些優化技術進行介紹。

4、檢視及分析JIT編譯結果

如何從外部觀察JVM的JIT編譯行為?

最好自己編譯Debug版本OpenJDK,有一些引數需要Debug或FastDebug版JVM的支援(Product版本不支援), 可以參考《CentOS上編譯OpenJDK8原始碼 以及 在eclipse上除錯HotSpot虛擬機器原始碼》。

相關引數如下:

"-XX:+PrintCompilation":要求JVM在JIT編譯時將衩編譯原生代碼的方法名稱打印出來;

"-XX:+PrintInlining":要求JVM輸出方法內聯資訊(Product版本需要"-XX:+UnlockDiagnosticVMOptions"選項,開啟JVM診斷模式);

"-XX:+PrintAssembly":JVM安裝反彙編介面卡後,該引數使得JVM輸出編譯方法的彙編程式碼(Product版本需要"-XX:+UnlockDiagnosticVMOptions"選項,開啟JVM診斷模式);

"-XX:+PrintLIR":輸出比較接近最終結果的中間程式碼表示,包含一些註釋資訊(用於C1,Debug版本);

"-XX:+PrintOptoAssembly":輸出比較接近最終結果的中間程式碼表示,包含一些註釋資訊(用於C2,非Product版本);

"-XX:+PrintCFGToFile":將JVM編譯過程中各個階段的資料輸出到檔案中,而後用工具C1 Visualizer分析(用於C1,Debug版本);

"-XX:+PrintIdealGraphFile":將JVM編譯過程中各個階段的資料輸出到檔案中,而後用工具IdealGraphVisualizer分析(用於C2,非Product版本);

5、JIT編譯優化技術

從前面對C1、C2編譯器的介紹可以知道,它們在編譯過程中採用了很多優化技術,HotSpot虛擬機器JIT編譯採用的優化技術可參考:

下面介紹幾種最具代表性的優化技術:

1、語言無關的經典優化技術之一:公共子表示式消除;

2、語言相關的經典優化技術之一:陣列範圍檢查消除;

3、最重要的優化技術之一:方法內聯;

4、最前沿的經典優化技術之一:逃逸分析;

5-1、公共子表示式消除

公共子表示式消除(Common Subexpression Elimination)是語言無關的經典優化技術之一,普遍應用於各種編譯器。

1、概述理解

如果一個表示式E已經被計算過了,並且從先前的計算到現在E中所有變數的值都沒有發生變化,那麼E的這次出現就稱為了公共子表示式

對於這種表示式,沒有必要花時間再對它進行計算,只需要直接用前面計算過的表示式結果代替E就可以了;

對於不同範圍,可分為區域性公共子表示式消除全域性公共子表示式消除

2、例項說明

int d = (c*b)*12+a+(a+b*c);

javac不會優化,JIT編譯優化如下:

1、公共子表示式消除:int d = E*12+a+(a+E);

2、代數化簡(Algebraic Simplification):int d = E*13+a*2;

5-2、陣列邊界檢查消除

陣列範圍檢查消除(Array Bounds Checking Elimination)是JIT編譯器中的一項語言相關的經典優化技術。

1、概念理解

Java訪問陣列的時候系統將會自動進行上下界的範圍檢查;

這對於虛擬機器的執行子系統來說,每次陣列元素的讀寫都帶有一次隱含的條件判定操作,對於擁有大量陣列訪問的程式程式碼,這無疑也是一種效能負擔;

陣列邊界檢查是必須做的,但陣列邊界檢查在某些情況下可以簡化;

2、例項說明

(A)、陣列下標是常量

foo[3];

編譯器根據資料流分析確定foo.length的值,並判斷下標"3"沒有越界,執行的時候就無須判斷了;

(B)、陣列下標是迴圈變數

for(int i …){

foo[i];

}

如果編譯器通過資料流分析就可以判定迴圈變數"0<=i< foo.length",那在整個迴圈中就可以把陣列的上下界檢查消除掉,這可以節省很多次的條件判斷操作。

3、隱式異常處理

大量的安全檢查很可能成為一個Java語言比C/C++更慢的因素;

要消除這些隱式開銷,可以:

(A)、儘可能把執行期檢查提到JIT編譯期完成,如前面的陣列邊界檢查消除

(B)、隱式異常處理如空指標檢查和算術運算中的除數為零的檢查

(i)、例項說明

 if(foo != null){
   return foo.value;
}else {
   throw new NullPointException();
}

隱式異常處理後,虛擬碼變為:

try{
   return foo.value;
}catch(seqment_fault){
   uncommon_trap();
}

不會進行空值判斷,JVM註冊異常處理器,為空的時候進入處理器恢復並丟擲異常

(ii)、隱式異常處理前後比較

隱式異常處理不會消耗一次空值判斷的開銷,但空值時處理經過使用者空間->核心空間->使用者空間,速度更慢,所以適用於很少為空值的情況

如果經常為空,還是不用隱式異常處理的好;

HotSpot執行時會收集Profile資訊,自動選擇最優方案;

與語言相關的其他消除操作還有自動裝箱消除(Autobox Elimination)、安全點消除(Safepoint Elimination)、消除反射(Dereflection)等。

5-3、方法內聯

方法內聯(Method Inlining)是編譯器最重要的優化手段之一,普遍適用於各種編譯器。

1、概念解理    

編譯器將程式中較小的、多次出現被呼叫的函式,用函式的函式體來直接進行替換函式呼叫表示式。

優點:

(A)、去除方法呼叫成本(如建立棧幀);

(B)、為其他優化建立良好的優化效果,方法內聯膨脹後便於在更大範圍進行優化;

2、例項說明

        static class B {
            int value;
            final int get() {
                return value;
            }
        }

        public void foo() {
            y = b.get();
            ……
            z = b.get();
            sum = y + z;
        }

(A)、方法內聯後

            public void foo() {
                y = b.value;
                ……
                z = b.value;
                sum = y + z;
            }

(B)、冗餘訪問消除(Redundant Loads Elimination)

            public void foo() {
                y = b.value;
                ……
                z = y;
                sum = y + z;
            }

(C)、複寫傳播(Copy Propagation)

            public void foo() {
                y = b.value;
                ……
                y = y;
                sum = y + y;
            }

(D)、無用程式碼消除(Dead Code Elimination)

            public void foo() {
                y = b.value;
                ……
                sum = y + y;
            }

例項揭示了方法內聯對其他優化手段的意義;

如果不做內聯,後續即使進行了無用程式碼消除的優化,也無法發現任何"Dead Code";

3、虛方法的內聯問題

Java中只有4種方法可以在編譯期進行解析:invokespecial指令呼叫的私有方法、例項構造器、父類方法以及invokestatic指令呼叫的靜態方法;

而對於一個虛方法,編譯期做內聯時無法確定應該使用哪個方法版本,需要在執行時進行方法接收者的多型選擇;

Java程式存在大量虛方法,如預設的例項方法(非fianl修飾),所以為解決虛方法的內聯問題,首先引用一種名為"型別繼承關係分析"(Class Hierarchy Analysis,CHA)的技術

(A)、如果是非虛方法,則直接進行內聯;

(B)、如果是虛方法,則向CHA查詢;

(i)、如果查詢結果只有一個版本,也可以進行內聯;

不過這屬於激進優化,需要預留一個"逃生門",稱為守護內聯(Guarded Inlining);

因為執行載入類可能導致繼承關係發生變化,需要退回解釋執行,或重新編譯;

(ii)、如果查詢結果有多個版本目標,使用內聯快取(Inline Cache)來完成方法內聯;

當快取第一次呼叫方法接收者資訊,以後每次呼叫都比較,不一致時取消內聯;

所以方法內聯是一種激進優化;

激進優化在商用虛擬機器中很常見,需要預留"逃生門",可以退回解釋執行

5-4、逃逸分析

逃逸分析(Escape Analysis)是JVM中比較前沿的優化技術;

並不是直接優化程式碼的手段,而是為其他優化手段提供依據的分析技術;

1、概念理解

逃逸分析的基本行為就是分析物件動態作用域:

(A)、方法逃逸

當一個物件在方法裡面被定義後,它可能被外部方法所引用,例如作為呼叫引數傳遞到其他方法中,這種行為稱為方法逃逸;

(B)、執行緒逃逸

甚至還有可能被外部執行緒訪問到,譬如賦值給類變數或可以在其他執行緒中訪問的例項變數,這種行為稱為執行緒逃逸;

2、優化說明

如果能證明一個物件不會逃逸到方法或執行緒之外,也就是別的方法或執行緒無法通過任何途徑訪問到這個物件,則可能為這個變數進行一些高效的優化,如:

(A)、棧上分配(Stack Allocations)

如果確定一個物件不會逃逸出方法之外,那讓這個物件在棧上分配記憶體將會是一個很不錯的主意,物件所佔用的記憶體空間就可以隨棧幀出棧而銷燬;

一般應用中,大多區域性物件都可以使用棧上分配,這樣垃圾收集器的壓力就會小很多;

(B)、同步消除(Synchronization Elimination)

執行緒同步本身就是一個相對耗時的過程,如果逃逸分析能夠確定一個變數不會逃逸出執行緒,無法被其他執行緒訪問,那這個變數的讀寫肯定就不會有競爭,對這個變數實施的同步措施就可以消除掉。

(C)、標量替換(Scalar Replacement)

標量(Scalar):

是指一個數據已經無法再分解成更小的資料來表示了,Java虛擬機器中的原始資料型別(int、long等 數值型別及reference型別等)都不能再進一步分解,它們就可以被稱為標量;

聚合量 (Aggregate):

相對的,如果一個數據可以繼續分解,那它就被稱作聚合量 (Aggregate),Java中的物件就是最典型的聚合量;

標量替換:

如果把一個Java物件拆散,根據程式訪問的情況,將其使用到的成員變數恢復原始型別來訪問就叫做標量替換;

如果逃逸分析證明一個物件不會被外部訪問,並且這個物件可以被拆散的話,那程式真正執行的時候將可能不建立這個物件,而改為直接建立它的若干個被這個方法使用到的成員變數來代替。

將物件拆分後,除了可以讓物件的成員變數在棧上(棧上儲存的資料,很大機會會被虛擬機器分配至物理機器的高速暫存器中儲存)分配和讀寫之外,還可以為後續進一步的優化手段建立條件。

3、技術實現

JDK1.6才實現逃逸分析,但至今都尚未足夠成熟,因為:

如果要完全準確判斷一個物件是否會逃逸,需要進行資料流敏感的一系列複雜分析;

這是一個相對高耗時的過程,不能保證逃逸分析的效能必定高於它的消耗;

所以現在只能採用不是太準確,但時間壓力相對較小的演算法來完成

如棧上分配實現起來比較複雜,HotSpot中暫時還沒有做這項優化;

4、相關引數

JDK6 u23的C2才開始預設開啟逃逸分析,相關引數如下:

"-XX:+/-DoEscapeAnalysis":開啟/關閉逃逸分析(只有Server VM支援);

"-XX:+PrintEscapeAnalysis":檢視逃逸分析結果(Server VM 非Product版本支援);

"-XX:+/-EliminateAllocations":在開啟逃逸分析情況下,開啟/關閉標量替換(只有Server VM支援);

"-XX:+PrintEliminateAllocations":檢視標量替換結果(Server VM 非Product版本支援);

6、Java與C/C++的編譯器對比

Java與C/C++的編譯器對比實際上代表了最經典的JIT編譯器與靜態編譯器的對比,很大程度上也決定了Java與C/C++的效能對比的結果;

Java虛擬機器的JIT編譯器輸出的原生代碼質量可能有一些劣勢,因為:

1、JIT編譯佔用的是使用者程式的執行時間,具有很大的時間壓力,不敢隨便引入大規模的優化技術;

2、Java語言是動態的型別安全語言,需要由虛擬機器來確保程式不會違反語言的語義或訪問非結構化記憶體;虛擬機器必須頻繁地進行動態檢查,消耗時間,如例項方法訪問時檢查空指標、陣列元素訪問時檢查上下界範圍、型別轉換時檢查繼承關係,等等;

3、Java程式使用虛方法的頻率卻遠遠大於C/C++語言,執行時需要對方法接收者進行多型選擇,這也意味著即時編譯器在進行一些優化時的難度要遠遠大於C/C++的靜態優化編譯器;

4、Java是可以動態擴充套件的語言,執行時載入新的類可能改變程式型別的繼承關係,這使得很多全域性的優化都難以進行,只能以激進優化的方式來完成,編譯器不得不時刻注意並隨著型別的變化而在執行時撤銷或重新進行一些優化;

5、Java語言中物件分配都在堆上,需要垃圾收集器自動回收管理,佔用資源;而C/C++的物件可能在棧上分配,並且主要由使用者程式程式碼來回收分配的記憶體,將減輕記憶體回收的壓力。

總得來說,Java在效能上劣勢都是為換取開發效率上的優勢而付出的代價

動態安全,動態擴充套件,垃圾回收這些"拖後腿"的特性都是為Java的開發效率做出了很大的貢獻;

何況Java即時編譯器能做的,C/C++的靜態優化編譯器不一定能夠做:

由於C/C++的靜態編譯,以執行效能監控為基礎的優化措施它都無法進行,如呼叫頻率預測,分支頻率預測,裁剪未使用分支等,這些都是稱為java語言獨有的效能優勢。

到這裡,我們大體瞭解Java的即時編譯技術,更多實現細節可以參考HotSpot原始碼,更多編譯技術原理可以參考《編譯原理》第二版(龍書)、《現代編譯原理》(虎書)、《高階編譯器設計與實現》(鯨書)。

後面我們將去了解Java記憶體回收--垃圾收集演算法及垃圾收集器……

【參考資料】

1、HotSpot原始碼

2、《編譯原理》第二版

3、《深入理解Java虛擬機器:JVM高階特性與最佳實踐》第二版 第11章