1. 程式人生 > >讀書筆記 ---- 《深入理解Java虛擬機器》---- 第10篇:晚期(執行期)優化

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第10篇:晚期(執行期)優化

上一篇:早期(編譯期)優化:https://blog.csdn.net/pcwl1206/article/details/84635959

目  錄:

1、HotSpot虛擬機器內的即時編譯器

1.1  直譯器與編譯器 

1.2  編譯物件與觸發條件

1.3  編譯過程

1.4  檢視及分析即時編譯結果

2  編譯優化技術

2.1  公共子表示式消除

2.2  陣列邊界檢查消除

2.3  方法內聯

2.4  逃逸分析

3  Java與C/C++的編譯器對比  --  瞭解即可

4  總結


Java程式在執行的期間,可能會有某個方法或者程式碼塊的執行特別頻繁時,就會把這些程式碼認定為“熱點程式碼”。為了提高熱點程式碼的執行效率,在執行時JVM會將這些程式碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler,JIT編譯器)。

1、HotSpot虛擬機器內的即時編譯器

在本小節中需要弄懂以下五個問題:

1、為何HotSpot虛擬機器要使用直譯器與編譯器並存的架構?

2、為何HotSpot虛擬機器要實現兩個不同的即時編譯器?

3、程式何時使用直譯器執行?何時使用編譯器執行?

4、哪些程式程式碼會被編譯為原生代碼?如何編譯為原生代碼?

5、如何從外部觀察即時編譯器的編譯過程和編譯結果?

1.1  直譯器與編譯器 

直譯器:程式可以迅速啟動和執行,消耗記憶體小 (類似人工,成本低,到後期效率低);

編譯器:隨著程式碼頻繁執行會將程式碼編譯成本地機器碼  (類似機器,成本高,到後期效率高)。

直譯器和編譯器是相互配合使用的。

【問題1答案:為何HotSpot虛擬機器要使用直譯器與編譯器並存的架構?

在整個虛擬機器執行架構中,直譯器與編譯器經常配合工作,兩者各有優勢:當程式需要迅速啟動和執行的時候,直譯器可以首先發揮作用,省去編譯的時間,立即執行。在程式執行後,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的程式碼編譯成原生代碼之後,可以獲取更高的執行效率

。當程式執行環境中記憶體資源限制較大(如部分嵌入式系統),可以使用解釋執行節約記憶體,反之可以使用編譯執行來提升效率。

同時,直譯器還可以作為編譯器激進優化時的一個“逃生門”,讓編譯器根據概率選擇一些大多數時候能提升執行速度的優化手段,當激進優化的假設不成立,如載入了新類後型別繼承結構出現變化、出現“罕見陷進”時可以通過逆優化退回到解釋狀態繼續執行。

解釋執行可以節約記憶體,而編譯執行可以提升效率。因此,在整個虛擬機器執行架構中,直譯器與編譯器經常配合工作。

目前主流的HotSpot虛擬機器中預設是採用直譯器與其中一個編譯器直接配合的方式工作。程式使用哪個編譯器,取決於虛擬機器執行的模式。HotSpot虛擬機器會根據自身版本與宿主及其的硬體效能自動選擇執行模式,使用者也可以使用“-client”或“-server”引數去強制指定虛擬機器執行在Client模式或Server模式。

【問題2答案:為何HotSpot虛擬機器要實現兩個不同的即時編譯器?

HotSpot虛擬機器中內建了兩個即時編譯器:Client ComplierServer Complier,簡稱為C1、C2編譯器,分別用在客戶端和服務端。為了在程式啟動響應速度與執行效率之間達到最佳平衡,HotSpot採用分層編譯策略,Client Compiler和Server  Compiler將會同時工作,許多程式碼都可能會被多次編譯,用Client Complier獲取更高的編譯速度,用Server Complier 來獲取更好的編譯質量。

分層編譯:

由於即時編譯器編譯原生代碼需要佔用程式執行時間,要編譯出優化程度更高的程式碼,所花費的時間可能更長;而且想要編譯出優化程度更高的程式碼,直譯器可能還要替編譯器收集效能監控資訊,這對解釋執行的速度也有影響。為了在程式啟動響應速度與執行效率之間達到最佳平衡,HotSpot虛擬機器會逐漸啟用分層編譯(Tiered  Compilation)的策略。JDK1.7中的Server模式虛擬機器中作為預設編譯策略被開啟。

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

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

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


1.2  編譯物件與觸發條件

【問題4答案:哪些程式程式碼會被編譯為原生代碼?如何編譯為原生代碼?

程式中的程式碼只有是“熱點”程式碼時,才會被編譯為原生代碼。在執行過程中會被即時編譯的“熱點程式碼”有兩類:

1、被多次呼叫的方法:這是由方法呼叫觸發的編譯,因此編譯器理會以整個方法作為編譯物件,這種編譯也是虛擬機器中標準的JIT編譯方式;

2、被多次執行的迴圈體:儘管編譯動作是由迴圈體所觸發的,但編譯器依然會以整個方法(而不是單獨的迴圈體)作為編譯物件。這種編譯方式因為編譯發生在方法執行過程之中,因此形象地稱之為棧上替換(On Stack Replacement,簡稱為OSR編譯,即方法棧幀還在棧上,方法就被替換了)。

判斷一段程式碼是不是“熱點”程式碼,是不是需要觸發即時編譯,這樣的行為稱為熱點探測,目前主要的熱點探測判定方式有兩種:

1、基於取樣的熱點探測:虛擬機器會週期性地檢查各個執行緒的棧頂,如果發現某個(或某些)方法經常出現在棧頂,那這個方法就是“熱點方法”。

優點:實現簡單、 高效,還可以很容易地獲取方法呼叫關係;

缺點:缺點是很難精確地確認一個方法的熱度,容易因為受到執行緒阻塞或別的外界因素的影響而擾亂熱點探測。

2、基於計數器的熱點探測:虛擬機器會為每個方法(甚至是程式碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值就認為它是“熱點方法”。

優點:統計結果相對來說更加精確和嚴謹;

缺點:實現起來麻煩,不能直接獲取到方法的呼叫關係。

在HotSpot虛擬機器中使用的是基於計數器的熱點探測方法,因此它為每個方法準備了兩類計數器:方法呼叫計數器回邊計數器。在確定虛擬機器執行引數的前提下,這兩個計數器都有一個確定的閾值,當計數器超過閾值溢位了,就會觸發JIT編譯。

1、方法呼叫計數器

統計方法被呼叫的次數,預設閾值在Client模式下是1500次,在Server模式下是10000次,這個閾值可以通過虛擬機器引數-XX:CompileThreshold來設定。

【問題3答案:程式何時使用直譯器執行?何時使用編譯器執行?

當一個方法被呼叫時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優先使用編譯後的原生代碼來執行。如果不存在已被編譯過的版本,則將此方法的呼叫計數器值加1,然後判斷方法呼叫計數器與回邊計數器值之和是否超過方法呼叫計數器的閾值。如果已超過閾值,那麼將會向即時編譯器提交一個該方法的程式碼編譯請求。

如果不做任何設定,執行引擎並不會同步等待編譯請求完成,而是繼續進入直譯器按照解釋方式執行位元組碼,直到提交的請求被編譯器編譯完成。當編譯工作完成之後,這個方法的呼叫入口地址就會被系統自動改寫成新的,下一次呼叫該方法時就會使用已編譯的版本。

回邊計數器:統計一個方法中迴圈體程式碼執行的次數。應用與迴圈體的回邊計數器與方法計數器的原理一樣,這裡就不在敘述。

如果不做任何設定,方法呼叫計數器統計的並不是方法被呼叫的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被呼叫的次數。當超過一定的時間限度,如果方法的呼叫次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的呼叫計數器就會被減少一半,這個過程稱為方法呼叫計數器熱度的衰減(Counter Decay),而這段時間就稱為此方法統計的半衰週期(Counter Half Life Time)。

可以使用虛擬機器引數-XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法呼叫的絕對次數,這樣,只要系統執行時間足夠長,絕大部分方法都會被編譯成原生代碼。另外,可以使用-XX:CounterHalfLifeTime引數設定半衰週期的時間,單位是秒。

2、回邊計數器

它的作用是統計一個方法中迴圈體程式碼執行的次數,在位元組碼中遇到控制流向後跳轉的指令稱為“回邊”(Back Edge)。 顯然,建立回邊計數器統計的目的就是為了觸發OSR編譯。可以通過-XX:OnStackReplacePercentage來間接調整回邊計數器的閾值。

當直譯器遇到一條回邊指令時,會先查詢將要執行的程式碼片段是否有已經編譯好的版本,如果有,它將會優先執行已編譯的程式碼,否則就把回邊計數器的值加1,然後判斷方法呼叫計數器與回邊計數器值之和是否超過回邊計數器的閾值。當超過閾值的時候,將會提交一個OSR編譯請求,並且把回邊計數器的值降低一些,以便繼續在直譯器中執行迴圈,等待編譯器輸出編譯結果。

回邊計數器沒有計數熱度衰減的過程,因此這個計數器統計的就是該方法迴圈執行的絕對次數。 當計數器溢位的時候,它還會把方法計數器的值也調整到溢位狀態,這樣下次再進入該方法的時候就會執行標準編譯過程。


1.3  編譯過程

在預設設定下,無論是方法呼叫產生的即時編譯請求,還是OSR編譯請求,虛擬機器在程式碼編譯器還未完成之前,都仍然按照解釋方式繼續執行,而編譯動作則在後臺的編譯執行緒中進行。使用者可以通過引數-XX:-BackgroundCompilation來禁止後臺編譯,在禁止後臺編譯後,一旦達到JIT的編譯條件,執行執行緒向虛擬機器提交編譯請求後將會一直等待,直到編譯過程完成後再開始執行編譯器輸出的原生代碼

Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的,下面對它們做分別講解:

1、Client  Compiler

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

第一個階段:一個平臺獨立的前端將位元組碼構造成一種高階中間程式碼表示(High Level Intermediate Representaion,HIR)。 HIR使用靜態單分配(Static Single Assignment,SSA)的形式來代表程式碼值,這可以使得一些在HIR的構造過程之中和之後進行的優化動作更容易實現。 在此之前編譯器會在位元組碼上完成一部分基礎優化,如方法內聯、常量傳播等優化將會在位元組碼被構造成HIR之前完成。

第二個階段:一個平臺相關的後端從HIR中產生低階中間程式碼表示(Low-Level Intermediate Representation,LIR),而在此之前會在HIR上完成另外一些優化,如空值檢查消除、範圍檢查消除等,以便讓HIR達到更高效的程式碼表示形式。

第三個階段:是在平臺相關的後端使用線性掃描演算法(Linear Scan Register Allocation)在LIR 上分配暫存器,並在LIR上做窺孔(Peephole)優化,然後產生機器程式碼。

2、Server  Compiler

Server Compiler則是專門面向服務端的典型應用併為服務端的效能配置特別調整過的編譯器,也是一個充分優化過的高階編譯器它會執行所有經典的優化動作:

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

2、迴圈展開(Loop Unrolling);

3、迴圈表示式外提(Loop Expression Hoisting);

4、消除公共子表示式(Common Subexpression Elimination);

5、常量傳播(Constant Propagation);

6、基本塊重排序(Basic Block Reordering);

7、還會實施與Java語言特性密切相關的優化技術,如範圍檢查消除(Range Check Elimination)、 空值檢查消除(Null Check Elimination等。 另外,還可能根據直譯器或Client Compiler提供的效能監控資訊,進行一些不穩定的激進優化,如守護內聯(Guarded Inlining)、分支頻率預測(Branch Frequency Prediction)等。

Server Compiler編譯速度依然遠遠超過傳統的靜態優化編譯器,而且它相對於Client Compiler 編譯輸出的程式碼質量有所提高,可以減少原生代碼的執行時間,從而抵消了額外的編譯時間開銷。


1.4  檢視及分析即時編譯結果

【第5個問題答案:如何從外部觀察即時編譯器的編譯過程和編譯結果?

通過虛擬機器提供的一些引數來輸出即時編譯和某些優化手段(如方法內聯)的執行狀況。

  • 引數-XX:+PrintCompilation要求虛擬機器在即時編譯時將被編譯成原生代碼的方法名稱打印出來

2  編譯優化技術

Java程式設計師有一個共識:以編譯方式執行原生代碼比解釋方式更快。原因是:虛擬機器設計團隊幾乎把對程式碼的所有優化措施都集中在了即時編譯器中。因此,一般來說,即時編譯器產生的原生代碼會比Javac產生的位元組碼更加優秀。

具體的即時編譯優化技術列表可參考書上P346-P347頁。

下面用一個案例來初步展示下其中幾種優化技術是如何發揮作用的。

說明:下述程式碼優化過程是建立在程式碼的某種中間表示或者機器碼之上的,這裡用Java形式是為了便於展示。

優化前的程式碼:

// 優化前的程式碼
static class B{
    int value;
    final int get(){
        return value;
    }
}

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

第一步:方法內聯:

// 內聯後的程式碼
public void foo(){
    y = b.value;
    // ...do stuff...
    z = b.value;
    sum = y + z;
}

方法內聯的兩個目的:

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

2、為其他優化建立良好的基礎,方法內聯膨脹之後可以便於在更大範圍上採取後續的優化手段,從而獲取更好的優化效果。

第二步:冗餘訪問消除

中間註釋的程式碼:... do  stuff...不會改變b.value的值了,那就可以把“z = b.value”替換為“z=y”,因為它們的值都是一致的了,這樣就不用再去訪問b物件的區域性變量了。

// 冗餘消除後的程式碼
public void foo(){
    y = b.value;
    // ...do stuff...
    z = y;
    sum = y + z;
}

第三步:複寫傳播

上面這段程式碼沒有必要使用一個額外的變數"z",它與變數"y"是完全相等的,因此可以使用"y"來代替"z"

// 複寫傳播的程式碼
public void foo(){
    y = b.value;
    // ...do stuff...
    y = y;
    sum = y + z;
}

第四步:無用程式碼消除

無用程式碼可能是永遠不會被執行的程式碼,也可能是完全沒有意義的程式碼,被稱為“Dead  Code”。上面程式碼中的“y=y”就是沒有意義的,可以消除。

// 無用程式碼消除後的程式碼
public void foo(){
    y = b.value;
    // ...do stuff...
    sum = y + z;
}

經過以上四步的優化,省略了很多語句,執行效率也會提高。

接下來介紹幾項最有代表性的優化技術是如何運作的,分別是:

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

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

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

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

2.1  公共子表示式消除

公共子表示式消除的含義是:如果一個表示式E已經計算過了,並且從先前的計算到現在E中所有變數的值都沒有發生變化,那麼E的這次出現就成為了公共子表示式。對於這種表示式,沒有必要花時間再對它們進行計算,只需要直接用前面計算過的表示式結果代替E就可以了。

假設有下面這段程式碼:

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

如果這段交給Javac編譯器則不會進行任何優化。但是當這段程式碼進入到虛擬機器即時編譯器後,它將進行如下優化:

編譯器檢測到“c * b”與“b* c”是一樣的表示式,而且在計算期間b與c的值是不變的,則表示式可優化為:

int d = E * 12 + a + (a + E);

編譯器還可能進行另外一種優化:代數簡化

int d = E * 13 + a * 2;

2.2  陣列邊界檢查消除

在Java語言中訪問陣列元素foo[i]的時候系統將會自動進行上下界的範圍檢查,即檢查i必須滿足i>=0&&i<foo.length這個條件,否則將丟擲一個執行時異常:java.lang.ArrayIndexOutOfBoundsException。 但是對於虛擬機器的執行子系統來說,每次陣列元素的讀寫都帶有一次隱含的條件判定操作,對於擁有大量陣列訪問的程式程式碼 – 效能負擔。

如果編譯器只要通過資料流分析就可以判定迴圈變數的取值範圍永遠在區間[0,foo.length)之內,那在整個迴圈中就可以把陣列的上下界檢查消除,這可以節省很多次的條件判斷操作。

要消除上訴的隱式開銷,除了如陣列邊界檢查優化這種儘可能把執行期檢查提到編譯期完成的思路之外,還有一種處理思路:隱式異常處理,Java中空指標檢查和算術運算中除數為零的檢查都採用了這種思路。

下面用一個案例來說明:

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

使用隱式異常優化後,虛擬機器會把上訴程式碼變為如下的虛擬碼:

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

虛擬機器會註冊一個Segment Fault訊號的異常處理器(虛擬碼中的uncommon_trap()),這樣當foo不為空的時候,對value的訪問是不會額外消耗一次對foo判空的開銷的。代價就是當foo真的為空時,必須轉入到異常處理器中恢復並丟擲NullPointException異常,這個過程必須從使用者態轉到核心態中處理,結束後再回到使用者態,速度遠比一次判空檢查慢。 當foo極少為空的時候,隱式異常優化是值得的,但假如foo經常為空的話,這樣的優化反而會讓程式更慢,HotSpot虛擬機器會根據執行期收集到的Profile資訊自動選擇最優方案。


2.3  方法內聯

方法內聯的兩個目的:

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

2、為其他優化建立良好的基礎,方法內聯膨脹之後可以便於在更大範圍上採取後續的優化手段,從而獲取更好的優化效果。

 方法內聯的優化行為只是把目標方法的程式碼“複製”到發起呼叫的方法之中,避免發生真實的方法呼叫而已。 但是即時編譯器其實還是做了很多工作的,否則無法進行內聯。 因為Java中只有使用invokespecial指令呼叫的私有方法、例項構造器、父類方法以及使用invokestatic指令進行呼叫的靜態方法才是在編譯期進行解析的,除了上述4種方法之外,其他的Java方法呼叫都需要在執行時進行方法接收者的多型選擇,並且都有可能存在多於一個版本的方法接收者(final方法使用invokevirtual指令呼叫,但也是非虛方法),簡而言之,Java語言中預設的例項方法是虛方法。對於一個虛方法,編譯期做內聯的時候根本無法確定應該使用哪個方法版本。

為了解決虛方法的內聯問題,Java虛擬機器引入了”型別繼承關係分析”(Class Hierarchy Analysis,CHA)技術,其基於整個應用程式的型別進行分析,用於確定在目前已載入的類中,某個介面是否有多於一種的實現,某個類是否存在子類、子類是否為抽象類等資訊。

編譯器在進行內聯時,如果是非虛方法,那麼直接進行內聯就可以了,這時候的內聯是有穩定前提保障的。如果遇到虛方法,則會向CHA查詢此方法在當前程式下是否有多個目標版本可供選擇,如果查詢結果只有一個版本,那也可以進行內聯,不過這種內聯就屬於激進優化,需要預留一個“逃生門”(Guard條件不成立時的Slow Path),稱為守護內聯(Guarded Inlining)。如果程式的後續執行過程中,虛擬機器一直沒有載入到會令這個方法的接收者的繼承關係發生變化的類,那這個內聯優化的程式碼就可以一直使用下去。但如果載入了導致繼承關係發生變化的新類,那就需要拋棄已經編譯的程式碼,退回到解釋狀態執行,或者重新進行編譯。

如果向CHA查詢出來的結果是有多個版本的目標方法可供選擇,則編譯器會使用內聯快取(Inline Cache)來完成方法內聯,這是建立在目標方法正常入口之前的快取,它的工作原理大致是:在未發生方法呼叫之前,內聯快取狀態為空,當第一次呼叫發生後,快取記錄下方法接收者的版本資訊,並且每次進行方法呼叫時都比較接收者版本,如果以後進來的每次呼叫的方法接收者版本都是一樣的,那這個內聯還可以一直用下去。如果方法接收者不一致則說明程式真正使用了虛方法的多型特性,這時才會取消內聯,查詢虛方法表進行方法分派。


2.4  逃逸分析

逃逸分析與型別繼承關係分析一樣,並不是直接優化程式碼的手段,而是為其他優化手段提供依據的分析技術

逃逸分析的基本行為就是分析物件動態作用域:當一個物件在方法中被定義後,它可能被外部方法所引用,例如作為呼叫引數傳遞到其他方法中,稱為方法逃逸。甚至還有可能被外部執行緒訪問到,譬如賦值給類變數或可以在其他執行緒中訪問的例項變數,稱為執行緒逃逸。如果能證明一個物件不會逃逸到方法或執行緒之外,也就是別的方法或執行緒無法通過任何途徑訪問到這個物件,則可能為這個變數進行一些高效的優化。 

1、棧上分配(Stack Allocation)

Java虛擬機器中,Java堆中的物件對於各個執行緒都是共享和可見的,只要持有這個物件的引用,就可以訪問堆中儲存的物件資料。虛擬機器的GC可以回收堆中不再使用的物件,但回收動作包括篩選可回收物件,回收和整理記憶體都需要耗費時間。如果確定一個物件不會逃逸出方法之外,那讓這個物件在棧上分配記憶體將會非常好。物件所佔用的記憶體空間就可以隨棧幀出棧而銷燬。在一般應用中,不會逃逸的區域性物件所佔的比例很大,如果能使用棧上分配,那大量的物件就會隨著方法的結束而自動銷燬了,GC的壓力將會小很多。但是需要說明的是HotSpot虛擬機器目前的實現方式導致棧上分配實現起來比較複雜,因此在HotSpot中暫時還沒有做這項優化。

2、同步消除(Synchronization Elimination)

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

3、標量替換(Scalar Replacement)

標量(Scalar)是指一個數據已經無法再分解成更小的資料來表示了,Java虛擬機器中的原始資料型別(int、long等數值型別以及reference型別等)都不能再進一步分解,它們就可以稱為標量。相對的,如果一個數據可以繼續分解,那它就稱作聚合量(Aggregate),Java中的物件就是最典型的聚合量。如果把一個Java物件拆散,根據程式訪問的情況,將其使用到的成員變數恢復原始型別來訪問就叫做標量替換。如果逃逸分析證明一個物件不會被外部訪問,並且這個物件可以被拆散的話,那程式真正執行的時候將可能不建立這個物件,而改為直接建立它的若干個被這個方法使用到的成員變數來代替。將物件拆分後,除了可以讓物件的成員變數在棧上(棧上儲存的資料,有很大的概率會被虛擬機器分配至物理機器的高速暫存器中儲存)分配和讀寫之外,還可以為後續進一步的優化手段建立條件。

逃逸分析在JDK1.6中不太成熟,主要是不能保證逃逸分析的效能收益必定高於它的消耗。如果要完全準確地判斷一個物件是否會逃逸,需要進行資料流敏感的一系列複雜分析,從而確定程式各個分支執行時對此物件的影響。這是一個相對高耗時的過程,如果分析完後發現沒有幾個不逃逸的物件,那這些執行期耗用的時間就白白浪費了,所以目前虛擬機器只能採用不那麼準確,但時間壓力相對較小的演算法來完成逃逸分析。棧上分配實現起來比較複雜。

如果有需要,並且確認對程式執行有益,使用者可以使用以下引數進行相關分析:

-XX:+DoEscapeAnalysis:手動開啟逃逸分析

-XX:+PrintEscapeAnalysis:檢視分析結果

-XX:+EliminateAllocations:開啟標量替換

+XX:+EliminateLocks:開啟同步消除

-XX:+PrintEliminateAllocations:檢視標量的替換情況


3  Java與C/C++的編譯器對比  --  瞭解即可

主要靠直譯器執行的Java語言效能確實比較低下,但是Java的即時編譯器能做得非常好。

Java虛擬機器的即時編譯器與C/C++的靜態優化編譯器相比,可能會由於下列這些原因而導致輸出的原生代碼有一些劣勢(下面列舉的也包括一些虛擬機器執行子系統的效能劣勢)

第一,因為即時編譯器執行佔用的是使用者程式的執行時間,具有很大的時間壓力,它能提供的優化手段也嚴重受制於編譯成本。如果編譯速度不能達到要求,那使用者將在啟動程式或程式的某部分察覺到重大延遲,這點使得即時編譯器不敢隨便引入大規模的優化技術,而編譯的時間成本在靜態優化編譯器中並不是主要的關注點。

第二Java語言是動態的型別安全語言,這就意味著需要由虛擬機器來確保程式不會違反語言語義或訪問非結構化記憶體。 從實現層面上看,這就意味著虛擬機器必須頻繁地進行動態檢查,如例項方法訪問時檢查空指標、 陣列元素訪問時檢查上下界範圍、型別轉換時檢查繼承關係等。對於這類程式程式碼沒有明確寫出的檢查行為,儘管編譯器會努力進行優化,但是總體上仍然要消耗不少的執行時間。

第三,Java語言中雖然沒有virtual關鍵字,但是使用虛方法的頻率卻遠遠大於C/C++語言,這意味著執行時對方法接收者進行多型選擇的頻率要遠遠大於C/C++語言,也意味著即時編譯器在進行一些優化(如前面提到的方法內聯)時的難度要遠大於C/C++的靜態優化編譯器。

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

第五,Java語言中物件的記憶體分配都是堆上進行的,只有方法中的區域性變數才能在棧上分配。而C/C++的物件則有多種記憶體分配方式,既可能在堆上分配,又可能在棧上分配,如果可以在棧上分配執行緒私有的物件,將減輕記憶體回收的壓力。另外,C/C++中主要由使用者程式程式碼來回收分配的記憶體,這就不存在無用物件篩選的過程,因此效率上(僅指執行效率,排除了開發效率)也比垃圾收集機制要高。

Java語言的這些效能上的劣勢都是為了換取開發效率上的優勢而付出的代價,動態安全、 動態擴充套件、 垃圾回收這些“拖後腿”的特性都為Java語言的開發效率做出了很大貢獻。

在C/C++中,別名分析(Alias Analysis)的難度就要遠高於Java。Java的型別安全保證了只要ClassA和ClassB沒有繼承關係,那物件objA和objB就絕不可能是同一個物件,即不會是同一塊記憶體兩個不同別名 。

除了別名分析外,由於C/C++編譯器所有優化都在編譯期完成,以執行期效能監控為基礎的優化措施它都無法進行,如呼叫頻率預測(Call Frequency Prediction)、分支頻率預測(Branch Frequency Prediction)、裁剪未被選擇的分支(Untaken Branch Pruning)等


4  總結

本文講解了虛擬機器的熱點探測方法、HotSpot的即時編譯器、編譯觸發條件、以及如何從虛擬機器外部觀察和分析JIT編譯的資料和結果,還選擇了幾種常見的編譯期優化技術進行講解。


上一篇:早期(編譯期)優化:https://blog.csdn.net/pcwl1206/article/details/84635959