第六章 位元組碼執行方式--解釋執行和JIT,位元組碼jit
註:主要參考自《分布式java應用:基礎與實踐》《深入理解Java虛擬機器(第二版)》
1、兩種執行方式:
- 解釋執行(運行期解釋位元組碼並執行)
- 編譯為機器碼執行(將位元組碼編譯為機器碼並執行,這個編譯過程發生在運行期,稱為JIT編譯)
- 強制使用該模式:-Xcomp,下面是兩種編譯模式
- client(即C1):只做少量效能開銷比高的最佳化,佔用記憶體少,適用於傳統型程式。
- server(即C2):進行了大量最佳化,佔用記憶體多,適用於服務端程式。會收集大量的運行時資訊。
注意:
- 32為機器預設選擇C1,可在啟動時添加-client或-server來指定,64位機器若CPU>2且實體記憶體>2G則預設為C2,否則為C1
- Hotspot JVM執行代碼的機制:對在執行過程中執行頻率高的代碼進行編譯,對執行頻率不高的代碼繼續解釋執行
2、解釋執行
查看 第三章 類檔案結構與javap的使用 中的inc()方法的執行
或者查看《深入瞭解java虛擬機器(第二版)》P272-P275
3、編譯執行
- 編譯的對象
- 方法
- 方法中的迴圈體
- OSR編譯:編譯整段代碼,但是只有迴圈體部分會執行機器碼,其他部分還是解釋執行
- 觸發條件(執行頻率大於多少)
- 方法調用計數器:方法被調用的次數
- client:1500 server:10000
- 該閾值可通過-XX:CompileThreshold來指定
- 這裡"方法調用的次數"是指一段時間(半衰周期)內的調用次數,如果半衰周期內,該次數沒有達到閾值,則該次數減半。
- -XX:-UseCounterDecay 關閉上述機制,即半衰周期的無窮大
- -XX:CounterHalfLifeTime 半衰周期
- 回邊計數器:迴圈體內迴圈代碼的執行次數(即for中代碼的迴圈的次數)
- client:13995 server:10700
- 該閾值可通過-XX:OnStackReplacePercent(注意該OSRP只是一個計算回邊計數閾值的中間值),回邊計數閾值
- client:CompileThreshold*OSRP/100
- server:CompileThreshold*(OSRP-InterPreterProfilePercentage)/100
- -XX:OnStackReplacePercent:140 InterPreterProfilePercentage:33
- 方法編譯執行
- 解譯器調用方法時,檢查是否有已經存在的編譯版本,如果有,執行機器碼,如果沒有,方法調用計數器+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; }View Code
說明:在上述代碼中,b是B的一個執行個體。
方法內聯之後,
public void foo() { y = b.value; //do something z = b.value; sum = y + z; }View Code
方法內聯的條件:
- 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; }View Code
4.1.3、複寫傳播
當然,在冗餘消除後,JIT對上述的代碼進行分析,發現變數z沒用(可以完全用y來代替),進行"複寫傳播"之後,
public void foo() { y = b.value; //do something y = y; sum = y + y; }View Code
4.1.4、無用代碼消除
在"複寫傳播"後,發現"y=y"是無用代碼,所以可以進行"無用代碼的消除"操作,消除之後,
public void foo() { y = b.value; //do something sum = y + y; }View Code
需要說明的是,這裡的"無用代碼的消除"是在前三部最佳化的基礎上來做的,而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(); }}View Code
首先分析Animal的整個"類型繼承關係",發現只有一個實作類別Cat,那麼在methodA(Animal animal)的代碼就可以最佳化為如下,
public void methodA(Animal animal){ System.out.println("cat eat fish"); }View Code
但是,如果之後在運行過程中,"類型繼承關係"發現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 afterView Code
若在//do after中(即前邊兩句代碼之後的所有代碼中)再沒有其他代碼訪問"point對象"了,則將"point對象"打散並進行標量替換,
int x = 1; int y = 2; System.out.println("point.x:" + x + ",point.y:" + y);View Code
好處:
- 如果對象中定義的所有變數有的並沒有被用到,"標量替換"可以節省記憶體
- 執行時,不需要尋找對象引用,速度會快
5.2、棧上分配
含義:確定一個方法的變數不會逃逸出當前方法之外(即該變數不會被其他方法引用),則該變數可以直接分配在棧上,隨方法執行結束,棧幀消失,該變數也消失,減輕GC壓力。
好處:
- 執行時,不需要根據對象引用去堆中找對象,速度會快
- 分配在棧上,隨方法執行結束,棧幀消失,該變數也消失,減輕GC壓力。
5.3、同步削除
含義:確定一個方法的變數不會逃逸出當前線程之外(即該變數不會被其他線程使用),則對於該變數的同步策略就消除掉,如下,
synchronized(cat){ //do xxx }View Code
若cat不會逃逸出當前線程,則同步塊可以去掉,如下,
//do xxxView Code
總結:
解譯器:
- 程式啟動速度比編譯快
- 節省記憶體(不需要編譯,所以不需要放置編譯後的機器碼)
JIT編譯器:
注意:
- 使用JIT而不是使用在編譯期直接編譯成機器碼,除瞭解釋器部分的兩條有點外,還為了在運行期收集資料,有目的的進行編譯