晚期(運行期)優化

晚期(運行期)優化



Start

  • “熱點代碼”(Hot Spot Code) – 運行特別頻繁的方法或代碼塊;爲了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,下文中簡稱JIT編譯器)。

HotSpot虛擬機內的即時編譯器

幾個問題

  • 爲何HotSpot虛擬機要使用解釋器與編譯器並存的架構?
  • 爲何HotSpot虛擬機要實現兩個不同的即時編譯器?
  • 程序何時使用解釋器執行?何時使用編譯器執行?
  • 哪些程序代碼會被編譯爲本地代碼?如何編譯爲本地代碼?
  • 如何從外部觀察即時編譯器的編譯過程和編譯結果?

解釋器與編譯器

  • 釋器與編譯器兩者各有優勢:當程序需要迅速啓動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程序運行後,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以獲取更高的執行效率。當程序運行環境中內存資源限制較大(如部分嵌入式系統中),可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率。
  • 編譯器根據概率選擇一些大多數時候都能提升運行速度的優化手段,當優化的假設不成立,出現“罕見陷阱”(Uncommon Trap)時可以通過逆優化(Deoptimization)退回到解釋狀態繼續執行。

ch11-compiler-interpreter.png-113.4kB

  • HotSpot虛擬機中內置了兩個即時編譯器,分別稱爲Client Compiler和Server Compiler,或者簡稱爲C1編譯器–Client 和C2編譯器– Server(也叫Opto編譯器)。HotSpot虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可以使用“-client”或“-server”參數去強制指定虛擬機運行在Client模式或Server模式。
  • 解釋器與編譯器搭配使用的方式在虛擬機中稱爲“混合模式”(Mixed Mode),用戶可以使用參數“-Xint”強制虛擬機運行於“解釋模式”(Interpreted Mode),這時編譯器完全不介入工作,全部代碼都使用解釋方式執行。另外,也可以使用參數“-Xcomp”強制虛擬機運行於“編譯模式”(Compiled Mode),這時將優先採用編譯方式執行程序,但是解釋器仍然要在編譯無法進行的情況下介入執行過程。
# darcy @ darcy-pc in ~ [9:50:43] 
$ java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode) # 混合模式

# darcy @ darcy-pc in ~ [9:50:47] 
$ java -Xint -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, interpreted mode) # 解釋模式

# darcy @ darcy-pc in ~ [9:51:01] 
$ java -Xcomp -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, compiled mode) # 編譯模式
  • 即時編譯器編譯本地代碼需要佔用程序運行時間,要編譯出優化程度更高的代碼,所花費的時間可能更長;而且想要編譯出優化程度更高的代碼,解釋器可能還要替編譯器收集性能監控信息,這對解釋執行的速度也有影響。 爲了在程序啓動響應速度與運行效率之間達到最佳平衡,HotSpot虛擬機還會逐漸啓用分層編譯(Tiered Compilation)的策略。
    • 第0層,程序解釋執行,解釋器不開啓性能監控功能(Profiling),可觸發第1層編譯。
    • 第1層,也稱爲C1編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,如有必要將加入性能監控的邏輯。
    • 第2層(或2層以上),也稱爲C2編譯,也是將字節碼編譯爲本地代碼,但是會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
  • 實施分層編譯後,Client Compiler和Server Compiler將會同時工作,許多代碼都可能會被多次編譯,用Client Compiler獲取更高的編譯速度,用Server Compiler來獲取更好的編譯質量,在解釋執行的時候也無須再承擔收集性能監控信息的任務。

編譯對象與觸發條件

  • “熱點代碼”有兩類,
    • 被多次調用的方法 – 這是由方法調用觸發的編譯,因此編譯器理會以整個方法作爲編譯對象,這種編譯也是虛擬機中標準的JIT編譯方式。
    • 被多次執行的循環體 – 儘管編譯動作是由循環體所觸發的,但編譯器依然會以整個方法(而不是單獨的循環體)作爲編譯對象。這種編譯方式因爲編譯發生在方法執行過程之中,因此形象地稱之爲棧上替換(On Stack Replacement,簡稱爲OSR編譯,即方法棧幀還在棧上,方法就被替換了)。
  • 判斷一段代碼是不是熱點代碼,是不是需要觸發即時編譯,這樣的行爲稱爲熱點探測(Hot Spot Detection);熱點探測判定方式有兩種
    • 基於採樣的熱點探測(Sample Based Hot Spot Detection) – 虛擬機會週期性地檢查各個線程的棧頂,如果發現某個(或某些)方法經常出現在棧頂,那這個方法就是“熱點方法”。 優點:實現簡單、 高效,還可以很容易地獲取方法調用關係; 缺點:缺點是很難精確地確認一個方法的熱度,容易因爲受到線程阻塞或別的外界因素的影響而擾亂熱點探測
    • 基於計數器的熱點探測(Counter Based Hot Spot Detection) – 虛擬機會爲每個方法(甚至是代碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值就認爲它是“熱點方法”。缺點:實現起來麻煩,不能直接獲取到方法的調用關係。優點:統計結果相對來說更加精確和嚴謹。
  • HotSpot虛擬機中使用的是基於計數器的熱點探測方法,因此它爲每個方法準備了兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back EdgeCounter)。確定虛擬機運行參數的前提下,這兩個計數器都有一個確定的閾值,當計數器超過閾值溢出了,就會觸發JIT編譯
    • 方法調用計數器 – 統計方法被調用的次數,默認閾值在Client模式下是1500次,在Server模式下是10 000次,這個閾值可以通過虛擬機參數-XX:CompileThreshold來設定。當一個方法被調用時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優先使用編譯後的本地代碼來執行。如果不存在已被編譯過的版本,則將此方法的調用計數器值加1,然後判斷方法調用計數器與回邊計數器值之和是否超過方法調用計數器的閾值。如果已超過閾值,那麼將會向即時編譯器提交一個該方法的代碼編譯請求。如果不做任何設置,執行引擎並不會同步等待編譯請求完成,而是繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成。當編譯工作完成之後,這個方法的調用入口地址就會被系統自動改寫成新的,下一次調用該方法時就會使用已編譯的版本。

ch11-jit-compile.png-137.2kB

  • 如果不做任何設置,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被調用的次數。當超過一定的時間限度,如果方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減少一半,這個過程稱爲方法調用計數器熱度的衰減(Counter Decay),而這段時間就稱爲此方法統計的半衰週期(Counter Half Life Time)。
  • 可以使用虛擬機參數-XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法調用的絕對次數,這樣,只要系統運行時間足夠長,絕大部分方法都會被編譯成本地代碼。另外,可以使用-XX:CounterHalfLifeTime參數設置半衰週期的時間,單位是秒。
  • 回邊計數器,它的作用是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲“回邊”(Back Edge)。 顯然,建立回邊計數器統計的目的就是爲了觸發OSR編譯。可以通過-XX:OnStackReplacePercentage來間接調整回邊計數器的閾值。

ch11-back-edge-counter.png-155.1kB

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

ch11-java-method-memory-model.png-207.3kB

編譯過程

  • 默認設置下,無論是方法調用產生的即時編譯請求,還是OSR編譯請求,虛擬機在代碼編譯器還未完成之前,都仍然將按照解釋方式繼續執行,而編譯動作則在後臺的編譯線程中進行。用戶可以通過參數-XX:-BackgroundCompilation來禁止後臺編譯,在禁止後臺編譯後,一旦達到JIT的編譯條件,執行線程向虛擬機提交編譯請求後將會一直等待,直到編譯過程完成後再開始執行編譯器輸出的本地代碼。Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的。

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)優化,然後產生機器代碼。

ch11-client-compile.png-171.3kB

Server Compiler

  • Server Compiler則是專門面向服務端的典型應用併爲服務端的性能配置特別調整過的編譯器,也是一個充分優化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數時的優化強度,它會執行所有經典的優化動作,
    • 無用代碼消除(Dead Code Elimination)、
    • 循環展開(Loop Unrolling)、
    • 循環表達式外提(Loop Expression Hoisting)、
    • 消除公共子表達式(Common Subexpression Elimination)、
    • 常量傳播(Constant Propagation)、
    • 基本塊重排序(Basic Block Reordering),
    • 還會實施與Java語言特性密切相關的優化技術,如範圍檢查消除(Range Check Elimination)、 空值檢查消除(Null Check Elimination,不過並非所有的空值檢查消除都是依賴編譯器優化的,有一些是在代碼運行過程中自動優化了)等。 另外,還可能根據解釋器或Client Compiler提供的性能監控信息,進行一些不穩定的激進優化,如守護內聯(Guarded Inlining)、分支頻率預測(Branch Frequency Prediction)等。
  • Server Compiler編譯速度依然遠遠超過傳統的靜態優化編譯器,而且它相對於Client Compiler
    編譯輸出的代碼質量有所提高,可以減少本地代碼的執行時間,從而抵消了額外的編譯時間開銷。

查看及分析即時編譯結果

  • 參數-XX:+PrintCompilation要求虛擬機在即時編譯時將被編譯成本地代碼的方法名稱打印出來
  • %的輸出說明是由回邊計數器觸發的OSR編譯;
public class Test {
    public static final int NUM = 15000;

    public static int doubleValue(int i) {
        // 這個空循環用於後面演示JIT代碼優化過程
        for(int j=0; j<100000; j++);
        return i * 2;
    }

    public static long calcSum() {
        long sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += doubleValue(i);
        }
        return sum;
    }

    public static void main(String[] args) {
        for (int i = 0; i < NUM; i++) {
            calcSum();
        }
    }
}
darcy@darcy-pc:~/IdeaProjects/jvm_in_action/src$ javac  ch11/Test.java 
darcy@darcy-pc:~/IdeaProjects/jvm_in_action/src$ java -XX:+PrintCompilation ch11.Test
     77    1       3       java.lang.String::hashCode (55 bytes)
     77    2       3       java.lang.String::equals (81 bytes)
     79    3       3       java.lang.String::charAt (29 bytes)
     79    4       3       java.lang.String::length (6 bytes)
     81    6       3       java.lang.Object::<init> (1 bytes)
     81    5       1       java.lang.ref.Reference::get (5 bytes)
     83    8     n 0       java.lang.System::arraycopy (native)   (static)
     83    7       3       java.lang.String::indexOf (70 bytes)
     83   11       3       java.lang.Math::min (11 bytes)
     83    9       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
     84   10       3       java.util.Arrays::copyOfRange (63 bytes)
     85   12       1       java.lang.ThreadLocal::access$400 (5 bytes)
     85   13       3       java.lang.AbstractStringBuilder::append (50 bytes)
    118   14 %     3       ch11.Test::doubleValue @ 2 (18 bytes)
    119   15       3       ch11.Test::doubleValue (18 bytes)
    119   16 %     4       ch11.Test::doubleValue @ 2 (18 bytes)
    120   14 %     3       ch11.Test::doubleValue @ -2 (18 bytes)   made not entrant
    120   17       4       ch11.Test::doubleValue (18 bytes)
    121   15       3       ch11.Test::doubleValue (18 bytes)   made not entrant
    121   18       3       ch11.Test::calcSum (26 bytes)
    122   19 %     4       ch11.Test::calcSum @ 4 (26 bytes)
    124   20       4       ch11.Test::calcSum (26 bytes)
    126   18       3       ch11.Test::calcSum (26 bytes)   made not entrant
darcy@darcy-pc:~/IdeaProjects/jvm_in_action/src$ 

編譯優化技術

  • 優化技術一覽

ch11-optimize-tech-3.png-219.9kB

ch11-optimize-tech-2.png.png-282.4kB

  • 針對某個例子的代碼優化
    • 方法內聯的重要性要高於其他優化措施,它的主要目的有兩個,一是去除方法調用的成本(如建立棧幀等),二是爲其他優化建立良好的基礎,方法內聯膨脹之後可以便於在更大範圍上採取後續的優化手段,從而獲取更好的優化效果。 因此,各種編譯器一般都會把內聯優化放在優化序列的最靠前位置。
    • 冗餘訪問消除(Redundant Loads Elimination),假設代碼中間註釋掉的“dostuff……”所代表的操作不會改變b.value的值,那就可以把“z=b.value”替換爲“z=y”,因爲上一句“y=b.value”已經保證了變量y與b.value是一致的,這樣就可以不再去訪問對象b的局部變量了。 如果把b.value看做是一個表達式,那也可以把這項優化看成是公共子表達式消除(Common Subexpression Elimination)
    • 複寫傳播(Copy Propagation),因爲在這段程序的邏輯中並沒有必要使用一個額外的變量“z”,它與變量“y”是完全相等的,因此可以使用“y”來代替“z”
    • 無用代碼消除(Dead Code Elimination)。無用代碼可能是永遠不會被執行的代碼,也可能是完全沒有意義的代碼
  • 經典優化技術
    • 語言無關的經典優化技術之一:公共子表達式消除。
    • 語言相關的經典優化技術之一:數組範圍檢查消除。
    • 最重要的優化技術之一:方法內聯。
    • 最前沿的優化技術之一:逃逸分析。

公共子表達式消除

  • 如果一個表達式E已經計算過了,並且從先前的計算到現在E中所有變量的值都沒有發生變化,那麼E的這次出現就成爲了公共子表達式。
  • 優化僅限於程序的基本塊內 – 局部公共子表達式消除(Local Common Subexpression Elimination); 優化的範圍涵蓋了多個基本塊 – 全局公共子表達式消除(Global CommonSubexpression Elimination)
int d= (c * b)*12+a+ (a + b * c)

# 編譯器檢測到“c * b”與“b* c”是一樣的表達式,而且在計算期間b與c的值是不變的。
int d=E*12+a+(a+E);

數組邊界檢查消除

  • 在Java語言中訪問數組元素foo[i]的時候系統將會自動進行上下界的範圍檢查,即檢查i必須滿足i>=0&&i<foo.length這個條件,否則將拋出一個運行時異常:java.lang.ArrayIndexOutOfBoundsException。 但是對於虛擬機的執行子系統來說,每次數組元素的讀寫都帶有一次隱含的條件判定操作,對於擁有大量數組訪問的程序代碼 – 性能負擔。
  • 如果編譯器只要通過數據流分析就可以判定循環變量的取值範圍永遠在區間[0,foo.length)之內,那在整個循環中就可以把數組的上下界檢查消除,這可以節省很多次的條件判斷操作。
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信息自動選擇最優方案。

方法內聯

  • 方法內聯的優化行爲只是把目標方法的代碼“複製”到發起調用的方法之中,避免發生真實的方法調用而已。 但是即時編譯器其實還是做了很多工作的,否則無法進行內聯 – 因爲Java中只有使用invokespecial指令調用的私有方法、實例構造器、父類方法以及使用invokestatic指令進行調用的靜態方法纔是在編譯期進行解析的,除了上述4種方法之外,其他的Java方法調用都需要在運行時進行方法接收者的多態選擇,並且都有可能存在多於一個版本的方法接收者(final方法使用invokevirtual指令調用,但也是非虛方法),簡而言之,Java語言中默認的實例方法是虛方法。對於一個虛方法,編譯期做內聯的時候根本無法確定應該使用哪個方法版本。
  • 爲了解決虛方法的內聯問題,Java虛擬機引入了”類型繼承關係分析”(Class Hierarchy Analysis,CHA)技術,其基於整個應用程序的類型進行分析,用於確定在目前已加載的類中,某個接口是否有多於一種的實現,某個類是否存在子類、子類是否爲抽象類等信息。編譯器在進行內聯時,如果是非虛方法,那麼直接進行內聯就可以了,這時候的內聯是有穩定前提保障的。如果遇到虛方法,則會向CHA查詢此方法在當前程序下是否有多個目標版本可供選擇,如果查詢結果只有一個版本,那也可以進行內聯,不過這種內聯就屬於激進優化,需要預留一個“逃生門”(Guard條件不成立時的Slow Path),稱爲守護內聯(Guarded Inlining)。如果程序的後續執行過程中,虛擬機一直沒有加載到會令這個方法的接收者的繼承關係發生變化的類,那這個內聯優化的代碼就可以一直使用下去。但如果加載了導致繼承關係發生變化的新類,那就需要拋棄已經編譯的代碼,退回到解釋狀態執行,或者重新進行編譯。
  • 如果向CHA查詢出來的結果是有多個版本的目標方法可供選擇,則編譯器會使用內聯緩存(Inline Cache)來完成方法內聯,這是建立在目標方法正常入口之前的緩存,它的工作原理大致是:在未發生方法調用之前,內聯緩存狀態爲空,當第一次調用發生後,緩存記錄下方法接收者的版本信息,並且每次進行方法調用時都比較接收者版本,如果以後進來的每次調用的方法接收者版本都是一樣的,那這個內聯還可以一直用下去。如果方法接收者不一致 – 說明程序真正使用了虛方法的多態特性,這時纔會取消內聯,查找虛方法表進行方法分派
public static void foo(Object object) {
    if (object != null) {
        System.out.println("ok.");
    }
}

public static void testInline(String[] args) {
    Object object = null;
    foo(object);
}

逃逸分析

  • 逃逸分析與類型繼承關係分析一樣,並不是直接優化代碼的手段,而是爲其他優化手段提供依據的分析技術.
  • 逃逸分析的基本行爲就是分析對象動態作用域:當一個對象在方法中被定義後,它可能被外部方法所引用,例如作爲調用參數傳遞到其他方法中,稱爲方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱爲線程逃逸。如果能證明一個對象不會逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個對象,則可能爲這個變量進行一些高效的優化。
    • 棧上分配(Stack Allocation):Java虛擬機中,Java堆中的對象對於各個線程都是共享和可見的,只要持有這個對象的引用,就可以訪問堆中存儲的對象數據。虛擬機的GC可以回收堆中不再使用的對象,但回收動作包括篩選可回收對象,回收和整理內存都需要耗費時間。如果確定一個對象不會逃逸出方法之外,那讓這個對象在棧上分配內存將會非常好 – 對象所佔用的內存空間就可以隨棧幀出棧而銷燬。在一般應用中,不會逃逸的局部對象所佔的比例很大,如果能使用棧上分配,那大量的對象就會隨着方法的結束而自動銷燬了,GC的壓力將會小很多。
    • 同步消除(Synchronization Elimination):線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那這個變量的讀寫肯定就不會有競爭,對這個變量實施的同步措施也就可以消除掉。
    • 標量替換(Scalar Replacement):標量(Scalar)是指一個數據已經無法再分解成更小的數據來表示了,Java虛擬機中的原始數據類型(int、long等數值類型以及reference類型等)都不能再進一步分解,它們就可以稱爲標量。相對的,如果一個數據可以繼續分解,那它就稱作聚合量(Aggregate),Java中的對象就是最典型的聚合量。如果把一個Java對象拆散,根據程序訪問的情況,將其使用到的成員變量恢復原始類型來訪問就叫做標量替換。如果逃逸分析證明一個對象不會被外部訪問,並且這個對象可以被拆散的話,那程序真正執行的時候將可能不創建這個對象,而改爲直接創建它的若干個被這個方法使用到的成員變量來代替。將對象拆分後,除了可以讓對象的成員變量在棧上(棧上存儲的數據,有很大的概率會被虛擬機分配至物理機器的高速寄存器中存儲)分配和讀寫之外,還可以爲後續進一步的優化手段創建條件。
  • 逃逸分析在JDK1.6中不太成熟 – 主要是不能保證逃逸分析的性能收益必定高於它的消耗。如果要完全準確地判斷一個對象是否會逃逸,需要進行數據流敏感的一系列複雜分析,從而確定程序各個分支執行時對此對象的影響。這是一個相對高耗時的過程,如果分析完後發現沒有幾個不逃逸的對象,那這些運行期耗用的時間就白白浪費了,所以目前虛擬機只能採用不那麼準確,但時間壓力相對較小的算法來完成逃逸分析。棧上分配實現起來比較複雜。
  • 如果有需要,並且確認對程序運行有益,用戶可以使用參數-XX:+DoEscapeAnalysis來手動開啓逃逸分析,開啓之後可以通過參數-XX:+PrintEscapeAnalysis來查看分析結果。有了逃逸分析支持之後,用戶可以使用參數-XX:+EliminateAllocations來開啓標量替換,使用+XX:+EliminateLocks來開啓同步消除,使用參數-XX:+PrintEliminateAllocations查看標量的替換情況。
  • Java中非逃逸對象的標量替換優化可以看做是一種高度優化後的棧上分配,但它相當於把對象拆散成局部變量再進行的棧上分配,而不是C/C++那種程序代碼可控的棧上分配方式。

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)等

ref

深入理解Java虛擬機(第二版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章