字節碼執行方式--解釋執行和JIT

此文已由做者趙計剛薪受權網易雲社區發佈。html

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


一、兩種執行方式:安全

  • 解釋執行(運行期解釋字節碼並執行)服務器

    • 強制使用該模式:-Xintscrapy

  • 編譯爲機器碼執行(將字節碼編譯爲機器碼並執行,這個編譯過程發生在運行期,稱爲JIT編譯)性能

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

    • client(即C1):只作少許性能開銷比高的優化,佔用內存少,適用於桌面程序。spa

    • server(即C2):進行了大量優化,佔用內存多,適用於服務端程序。會收集大量的運行時信息。.net

注意:線程

  • 32爲機器默認選擇C1,可在啓動時添加-client或-server來指定,64位機器若CPU>2且物理內存>2G則默認爲C2,不然爲C1

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

查看當前機器默認是client模式仍是server模式,使用:"java -version"命令,以下



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

二、解釋執行

查看 第三章 類文件結構與javap的使用 中的inc()方法的執行

或者查看《深刻了解java虛擬機(第二版)》P272-P275

 

三、編譯執行

  • 編譯的對象

    • 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,而後判斷回邊計數器是否超過閾值,若超過,進行編譯,後臺線程進行編譯,前臺線程繼續解釋執行(即不會阻塞),直到下一次執行到循環體時,若是編譯好了,就直接執行機器碼,若是沒編譯好,就解釋執行。

     

    四、C1優化

    說明:關於所有的優化技術列表,查看《深刻理解java虛擬機(第二版)》P346-P347

    只作少許性能開銷比高的優化,佔用內存少,主要的優化包括:

    • 方法內聯

    • 冗餘消除

    • 複寫傳播

    • 消除無用代碼

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

    • 去虛擬化

    4.一、方法內聯、冗餘消除、複寫傳播、消除無用代碼

    4.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.二、冗餘消除

    冗餘消除:如上邊的兩個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.三、複寫傳播

    固然,在冗餘消除後,JIT對上述的代碼進行分析,發現變量z沒用(能夠徹底用y來代替),進行"複寫傳播"以後,

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

    4.1.四、無用代碼消除

    在"複寫傳播"後,發現"y=y"是無用代碼,因此能夠進行"無用代碼的消除"操做,消除以後,

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

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

     

    4.二、類型繼承關係分析、去虛擬化

    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.好比"去虛擬化",若是編譯以後,發現類的實現方法多於一種了,此時就要執行"逆優化"

     

    五、C2優化

    進行了大量優化,佔用內存多,適用於服務端程序,對於C2優化,除了具備C1的優化措施後,還有不少優化。

    逃逸分析(輔助):

    開啓:-XX:+DoEscapeAnalysis

    根據運行情況來判斷方法中的變量是否會被方法或外部線程所讀取,若不會,此變量是不逃逸的。基於此,C2在編譯時會作:

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

    • 棧上分配

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

    5.一、標量替換

    含義:將一個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.二、棧上分配

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

    好處:

    • 執行時,不須要根據對象引用去堆中找對象,速度會快

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

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

    5.三、同步削除

    含義:肯定一個方法的變量不會逃逸出當前線程以外(即該變量不會被其餘線程使用),則對於該變量的同步策略就消除掉,以下,

        synchronized(cat){
            //do xxx
        }

    若cat不會逃逸出當前線程,則同步塊能夠去掉,以下,

    //do xxx

     

    總結:

    解釋器:

    • 程序啓動速度比編譯快

    • 節省內存(不須要編譯,因此不須要放置編譯後的機器碼)

    JIT編譯器:

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

    注意:

    • 使用JIT而不是使用在編譯期直接編譯成機器碼,除了解釋器部分的兩條有點外,還爲了在運行期收集數據,有目的的進行編譯


    免費領取驗證碼、內容安全、短信發送、直播點播體驗包及雲服務器等套餐

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


    相關文章:
    【推薦】 用scrapy數據抓取實踐

    相關文章
    相關標籤/搜索