1. 程式人生 > >位元組碼執行方式--解釋執行和JIT

位元組碼執行方式--解釋執行和JIT

此文已由作者趙計剛薪授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。


1、兩種執行方式:

  • 解釋執行(執行期解釋位元組碼並執行)

    • 強制使用該模式:-Xint

  • 編譯為機器碼執行(將位元組碼編譯為機器碼並執行,這個編譯過程發生在執行期,稱為JIT編譯)

    • 強制使用該模式:-Xcomp,下面是兩種編譯模式

    • client(即C1):只做少量效能開銷比高的優化,佔用記憶體少,適用於桌面程式。

    • server(即C2):進行了大量優化,佔用記憶體多,適用於服務端程式。會收集大量的執行時資訊。

注意:

  • 32為機器預設選擇C1,可在啟動時新增-client或-server來指定,64位機器若CPU>2且實體記憶體>2G則預設為C2,否則為C1

  • Hotspot JVM執行程式碼的機制:對在執行過程中執行頻率高的程式碼進行編譯,對執行頻率不高的程式碼繼續解釋執行

檢視當前機器預設是client模式還是server模式,使用:"java -version"命令,如下

2018120615040891fde534-f758-48d6-85fc-da116da69891.png


其中,mixed mode表示"解釋執行+編譯執行"的混合模式

2、解釋執行

檢視 第三章 類檔案結構與javap的使用 中的inc()方法的執行

或者檢視《深入瞭解java虛擬機器(第二版)》P272-P275

 

3、編譯執行

  • 編譯的物件

    • OSR編譯:編譯整段程式碼,但是隻有迴圈體部分會執行機器碼,其他部分還是解釋執行

    • 方法

    • 方法中的迴圈體

  • 觸發條件(執行頻率大於多少)

    • client:13995  server:10700

    • 該閾值可通過-XX:OnStackReplacePercent(注意該OSRP只是一個計算回邊計數閾值的中間值),回邊計數閾值

    • client:CompileThreshold*OSRP/100

    • server:CompileThreshold*(OSRP-InterPreterProfilePercentage)/100

    • -XX:OnStackReplacePercent:140  InterPreterProfilePercentage:33

    • client:1500  server:10000 

    • 該閾值可通過-XX:CompileThreshold來指定

    • 這裡"方法呼叫的次數"是指一段時間(半衰週期)內的呼叫次數,如果半衰週期內,該次數沒有達到閾值,則該次數減半。


    • -XX:-UseCounterDecay 關閉上述機制,即半衰週期的無窮大

    • -XX:CounterHalfLifeTime 半衰週期

    • 方法呼叫計數器:方法被呼叫的次數

    • 回邊計數器:迴圈體內迴圈程式碼的執行次數(即for中程式碼的迴圈的次數)

  • 方法編譯執行

    • 直譯器呼叫方法時,檢查是否有已經存在的編譯版本,如果有,執行機器碼,如果沒有,方法呼叫計數器+1,然後判斷方法呼叫計數器是否超過閾值,若超過,進行編譯,後臺執行緒進行編譯,前臺執行緒繼續解釋執行(即不會阻塞),直到下一次呼叫方法時,如果編譯好了,就直接執行機器碼,如果沒編譯好,就解釋執行。

  • 迴圈體編譯執行

    • 直譯器執行到迴圈體時,檢查是否有已經存在的編譯版本,如果有,執行機器碼,如果沒有,回邊計數器+1,然後判斷回邊計數器是否超過閾值,若超過,進行編譯,後臺執行緒進行編譯,前臺執行緒繼續解釋執行(即不會阻塞),直到下一次執行到迴圈體時,如果編譯好了,就直接執行機器碼,如果沒編譯好,就解釋執行。

 

4、C1優化

說明:關於全部的優化技術列表,檢視《深入理解java虛擬機器(第二版)》P346-P347

只做少量效能開銷比高的優化,佔用記憶體少,主要的優化包括:

  • 方法內聯

  • 冗餘消除

  • 複寫傳播

  • 消除無用程式碼

  • 型別繼承關係分析(CHA,輔助)

  • 去虛擬化

4.1、方法內聯、冗餘消除、複寫傳播、消除無用程式碼

4.1.1、方法內聯

方法內聯含義:假設方法A呼叫了方法B,把B的指令直接植入到A中。

    static class B{
        int value;
        final int get() {
            return value;
        }
    }
    
    public void foo() {
        y = b.get();
        //do something
        z = b.get();
        sum = y + z;
    }

說明:在上述程式碼中,b是B的一個例項。

方法內聯之後,

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

方法內聯的條件:

  • get()編譯後的位元組數<=35byte(預設) -XX:MaxInlineSize=35指定

方法內聯的地位:

  • 優化系列中最一開始使用的方式(因為是很多其他優化手段的基礎)

  • 消除方法呼叫的成本(建立棧幀、避免參數傳遞、避免返回值傳遞、避免跳轉)

4.1.2、冗餘消除

冗餘消除:如上邊的兩個b.value冗餘(前提,在do something部分沒有對b.value進行操作,這也是我們在做優化之前需要先收集資料的原因)

假設在do something部分沒有對b.value進行操作,進行冗餘消除後,

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

4.1.3、複寫傳播

當然,在冗餘消除後,JIT對上述的程式碼進行分析,發現變數z沒用(可以完全用y來代替),進行"複寫傳播"之後,

    public void foo() {
        y = b.value;
        //do something
        y = y;
        sum = y + y;
    }

4.1.4、無用程式碼消除

在"複寫傳播"後,發現"y=y"是無用程式碼,所以可以進行"無用程式碼的消除"操作,消除之後,

    public void foo() {
        y = b.value;
        //do something
        sum = y + y;
    }

需要說明的是,這裡的"無用程式碼的消除"是在前三部優化的基礎上來做的,而javac編譯中"語義分析"部分的"無用程式碼的消除"是直接消除一些直接寫好的程式碼(例如:if(false){})

 

4.2、型別繼承關係分析、去虛擬化

public interface Animal {
    public void eat();
}

public class Cat implements Animal{
    public void eat() {
        System.out.println("cat eat fish");
    }
}

public class Test{
    public void methodA(Animal animal){
        animal.eat();
    }
}

首先分析Animal的整個"型別繼承關係",發現只有一個實現類Cat,那麼在methodA(Animal animal)的程式碼就可以優化為如下,

    public void methodA(Animal animal){
        System.out.println("cat eat fish");
    }

但是,如果之後在執行過程中,"型別繼承關係"發現Animal又多了一個實現類Dog,那麼此時就不在執行之前優化編譯好的機器碼了,而是進行解釋執行,即如下的"逆優化"。

逆優化:

當編譯後的機器碼的執行不再符合優化條件,則該機器碼對應的部分回到解釋執行。

eg.比如"去虛擬化",如果編譯之後,發現類的實現方法多於一種了,此時就要執行"逆優化"

 

5、C2優化

進行了大量優化,佔用記憶體多,適用於服務端程式,對於C2優化,除了具有C1的優化措施後,還有很多優化。

逃逸分析(輔助):

開啟:-XX:+DoEscapeAnalysis

根據執行狀況來判斷方法中的變數是否會被方法或外部執行緒所讀取,若不會,此變數是不逃逸的。基於此,C2在編譯時會做:

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

  • 棧上分配

  • 同步削除:開啟 -XX:+EliminateLocks

5.1、標量替換

含義:將一個java物件打散,根據程式,將該物件中的屬性作為一個個標量來使用。

    Point point = new Point(1,2);
    System.out.println("point.x:" + point.x + ",point.y:" + point.y);
    //do after

若在//do after中(即前邊兩句程式碼之後的所有程式碼中)再沒有其他程式碼訪問"point物件"了,則將"point物件"打散並進行標量替換,

    int x = 1;
    int y = 2;
    System.out.println("point.x:" + x + ",point.y:" + y);

好處:

  • 如果物件中定義的所有變數有的並沒有被用到,"標量替換"可以節省記憶體

  • 執行時,不需要尋找物件引用,速度會快

5.2、棧上分配

含義:確定一個方法的變數不會逃逸出當前方法之外(即該變數不會被其他方法引用),則該變數可以直接分配在棧上,隨方法執行結束,棧幀消失,該變數也消失,減輕GC壓力。

好處:

  • 執行時,不需要根據物件引用去堆中找物件,速度會快

  • 分配在棧上,隨方法執行結束,棧幀消失,該變數也消失,減輕GC壓力。

  • 使用棧上分配,必須開啟標量替換

5.3、同步削除

含義:確定一個方法的變數不會逃逸出當前執行緒之外(即該變數不會被其他執行緒使用),則對於該變數的同步策略就消除掉,如下,

    synchronized(cat){
        //do xxx
    }

若cat不會逃逸出當前執行緒,則同步塊可以去掉,如下,

//do xxx

 

總結:

直譯器:

  • 程式啟動速度比編譯快

  • 節省記憶體(不需要編譯,所以不需要放置編譯後的機器碼)

JIT編譯器:

  • 時間長了,對於"熱點程式碼"的執行會快

注意:

  • 使用JIT而不是使用在編譯期直接編譯成機器碼,除了直譯器部分的兩條有點外,還為了在執行期收集資料,有目的的進行編譯


免費領取驗證碼、內容安全、簡訊傳送、直播點播體驗包及雲伺服器等套餐

更多網易技術、產品、運營經驗分享請點選


相關文章:
【推薦】 資料探勘與資料分析的主要區別