最近聽個人導師他們討論Java的即時編譯器(JIT),當時並不知道這是啥東西,因此就藉着週末的時間,學習了一下!java
在部分的商用虛擬機(Sun HotSpot)中,Java程序最初是經過解釋器(Interpreter)進行解釋執行的,當虛擬機發現某個方法或代碼塊運行的特別頻繁時,就會把這些代碼認定爲「熱點代碼」(Hot Spot Code)。爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些熱點代碼編譯成與本地代碼相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,下文中簡稱JIT編譯器)。程序員
即時編譯器並非虛擬機必須的部分,Java虛擬機規範並無規定Java虛擬機內必需要有即時編譯器存在,更沒有規定或指導即時編譯器應該如何去實現。可是,即時編譯器編譯性能的好壞、代碼優化程度的高低倒是衡量一款商用虛擬機優秀與否的最關鍵的指標之一,它是虛擬機中最核心且最能體現虛擬機技術水平的部分。編程
儘管並非全部的Java虛擬機都採用解釋器與編譯器並存的架構,但許多主流的商用虛擬機,如HtoSpot,J9等,都同時包含解釋器與編譯器。解釋器與編譯器二者各有優點:當程序須要迅速啓動和執行的時候,解釋器能夠首先發揮做用,省去編譯的時間,當即執行。在程序運行後,隨着時間的推移,編譯器逐漸發揮做用,把愈來愈多的代碼編程成本地代碼以後,獲取更搞得執行效率。當程序運行環境中內存資源限制較大,可使用解釋器執行節約內存,反之可使用編譯執行來提高效率。同時,解釋器還能夠做爲編譯器進行優化時的一個「逃生門」,讓編譯器根據機率選擇一些大多數時候都能提高運行速度的優化手段,當激進優化的假設不成立,如加載了新類後類型繼承結構出現變化、出現了「罕見陷阱」時能夠經過逆優化退回到解釋狀態繼續執行(部分沒有解釋器的虛擬機中也會採用不進行激進優化的C1編譯器--擔任「逃生門」的角色),所以,在整個虛擬機執行架構中,解釋器與編譯器常常配合工做,如圖所示:數組
HotSpot虛擬機中內置有兩個即時編譯器,分別稱爲Client Compiler和Server Compiler,或者簡稱爲C1編譯器和C2編譯器。目前主流的HotSpot虛擬機中,默認採用解釋器與其中一個編譯器直接配合的方式工做,程序使用哪一個編譯器,取決於虛擬機運行模式,HotSpot虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可使用「-client」或「-server」參數指定虛擬機運行在Client模式或Server模式!安全
虛擬機默認採用「混合模式」進行Java代碼編譯後執行,可使用 -Xint優先採用解釋器解釋執行;使用-Xcomp 優先採用即時編譯器編譯執行,可是解釋器仍然要在編譯沒法進行的狀況下介入執行過程!ruby
因爲即時編譯器編譯本地代碼須要佔用程序運行時間,要編譯出優化程度更高的代碼,所花費的時間可能更長;並且想要編譯出優化程度更高的代碼,解釋器可能還要替編譯器收集性能監控信息,這對解釋執行的速度也有影響。爲了在程序啓動響應速度與運行效率之間達到最佳平衡,HotSpot虛擬機還會逐漸啓動分層編譯的策略,分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不一樣的編譯層次,其中包括:架構
第0層,程序解釋執行,解釋器不開啓性能監控功能(Profiling),可觸發第1層編譯。函數
第1層,也稱爲C1編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,若有必要將加入性能監控的邏輯;性能
第2層,也稱爲C2編譯,也是將字節碼編譯爲本地代碼,可是會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。學習
實施分層編譯後,Client Compiler和Server Compiler將同時工做,許多代碼均可能會被屢次編譯,用Client Compiler獲取更高的編譯速度,用Server Compiler獲取更好的編譯質量,在解釋執行的時候也無需再承擔收集性能監控信息的任務!
上文中提到過,在運行過程當中會被即時編譯器編譯的「熱點代碼」有兩類:
1)被屢次調用的方法;
2)被屢次執行的循環體;
對於第一種狀況,因爲是由方法調用觸發的編譯,所以編譯器理所固然地會以整個方法做爲編譯對象,這種編譯也是虛擬機中標準得JIT編譯方法。而對於後一種狀況,儘管編譯動做由循環體所觸發的,但編譯器依然會以整個方法(而不是單獨的循環體)做爲編譯對象。這種編譯方式由於編譯發生在方法執行的過程當中,所以形象的稱之爲棧上替換(On Stack Replacement,簡稱OSR編譯,即方法棧幀還在棧上,方法就被替換了)。
基於採樣的熱點探測(Sample Based Hot Spot Detection):採用這種方法的虛擬機會週期性地檢測各個線程的棧頂,若是發現某個(或某些)方法常常出如今棧頂,那這個方法就是「熱點方法」。基於採樣的熱點探測的好處是實現簡單、高效、還能夠很容易地獲取方法調用關係,缺點就是很難精確地確認一個方法的熱度,容易由於受到線程阻塞或別的外界因素的影響而擾亂熱點探測;
基於計數器的熱點探測(Counter Based Hot Spot Detection):採用這種方法的虛擬機會爲每一個方法(甚至是代碼塊)創建計數器,統計代碼的執行次數,若是執行次數超過必定的閾值就認爲它是「熱點方法」。這種統計方法實現起來麻煩一些,須要爲每一個方法創建並維護計數器,並且不能直接獲取到方法的調用關係,可是它的統計結果相對來講更加精確和嚴格!
在HotSpot虛擬機中使用的第二種---基於計數器的熱點探測方法,所以它爲每一個方法準備了兩類計數器:方法調用計數器(Invocation counter)和回邊計數器(Back Edge counter)。當計數器超過閾值溢出了,就會觸發JIT編譯。
回邊計數器:它的做用是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲」回邊「。目的就是爲了觸發OSR編譯。
默認在Client模式下,回邊計數器的閾值是13995;在Server模式下的閾值是10700。
而Server Compiler則是專門面向服務端的典型應用併爲服務端的性能配置特別調整過的編譯器,它執行全部經典的優化動做,如無用代碼消除、循環展開、常量傳播等。以即時編譯器的標準來看,Server Compiler無疑是比較緩慢的,但它的編譯速度依然遠遠超過傳統的靜態優化編譯器,並且它相對於Client Compiler編譯輸出的代碼質量有所提升,能夠減小本地代碼的執行時間,從而抵消了額外的編譯時間開銷,因此也有不少非服務端的應用選擇使用Server模式的虛擬機運行。
Java程序員有一個共識,以編譯方式執行本地代碼比解釋方式更快,之因此有這樣的共識,除去虛擬機解釋執行字節碼時額外消耗時間的緣由外,還有一個很重要的緣由就是虛擬機設計團隊幾乎把代碼的全部優化措施都集中在了即時編譯器之中,所以通常來講,即時編譯器生成的本地代碼比Javac產生的字節碼更加優秀!
方法內聯具備很高的重要性,一是去除方法調用的成本(如創建棧幀等),而是爲了其餘優化創建良好的基礎!
注意:方法的調用過程發生的時期?
(1) 首先會有個執行棧,存儲目前全部活躍的方法,以及它們的本地變量和參數;
(2) 當一個新的方法被調用了,一個新的棧幀會被加到當前線程的棧頂,分配的本地變量和參數會存儲在這個棧幀中;
(3) 跳到目標方法代碼執行;
(4) 方法返回的時候,本地方法和參數會被銷燬,棧頂被移除;
(5) 返回原來地址執行;
方法內聯就是把被調用方函數代碼」複製」到調用方函數中,來減小因函數調用開銷的技術。
咱們寫一個簡單的兩數相加程序,被內聯前的代碼:
private int add1(int a, int b, int c, int d){ return add2(a + b) + add2(c + d); } private int add2(int x, int y){ return x + y; }
運行一段時間後JVM會把add2方法去掉,並把你的代碼翻譯成:
private int add1(int a, int b, int c, int d){ return a + b + c + d; }
1)方法內聯的其餘隱含條件:
雖然JIT號稱能夠針對代碼全局的運行狀況而優化,可是JIT對一個方法內聯以後,仍是可能由於方法被繼承,致使須要類型檢查而沒有達到性能的效果
想要對熱點的方法使用上內聯的優化方法,最好儘可能使用final、private、static
這些修飾符修飾方法,避免方法由於繼承,致使須要額外的類型檢查,而出現效果很差狀況。
2)若是想要知道方法被內聯的狀況,可使用下面的JVM參數來配置:
-XX:+PrintCompilation //在控制檯打印編譯過程信息
-XX:+UnlockDiagnosticVMOptions //解鎖對JVM進行診斷的選項參數。默認是關閉的,開啓後支持一些特定參數對JVM進行診斷
-XX:+PrintInlining //將內聯方法打印出來
3)編譯器在進行內聯時,若是是非虛方法,那麼直接進行內聯就能夠了,這時候的內聯是有穩定前提保障的。(虛方法即爲子類繼承/實現父類,重寫的方法)
1)優化前的原始代碼:
public class B{ int value; final int get(){ return value; } public void foo(){ y = b.get(); //......do stuff...... y = b.get(); sum = y + z; } }
2)內聯後的代碼:
public void foo(){ y = b.value //......do stuff...... z = b.value sum = y + z; }
3)冗餘diaman存儲消除的代碼:
public void foo(){ y = b.value //......do stuff...... z = y sum = y + z; }
把」z=b.value」替換爲」z=y「,由於上一句」y=b.value「已經保證了變量y和b.value是一致的,這樣就能夠再也不去訪問對象b的局部變量了!
4)複寫傳播代碼:
public void foo(){ y = b.value //......do stuff...... y = y sum = y + y; }
5)進行無用代碼消除:
public void foo(){ y = b.value //......do stuff...... sum = y + y; }
若是一個表達式E已經計算過了,而且從先前的計算到如今E中全部變量的值都沒有發生變化,那麼E的此次出現就成爲了公共子表達式!
例如:int d = (c + b) * 12 + a + (a + b * c);
1)代碼交給Javac編譯器則不會進行任何優化:(Javac編譯器編譯後的字節碼展現)
2)代碼進入即時編譯器JIT中:
int d = E * 12 + a + (a + E);
3) 這時即時編譯器有可能進行另一種優化:代數簡化
int d = E * 13 + a * 2;
Java語言是一門動態安全的語言。若是有一個數組foo[],在Java語言中訪問數組元素foo[i]的時候系統將會自動進行上下界的範圍檢查,即檢查i必須知足i >=0 && i < foo.length這個條件,不然將拋出一個運行時異常:java.lang.ArrayIndexOutOfBoundsException。
不管如何,爲了安全,數組便捷檢查確定是必須作的,但數組邊界檢查是否是必須在運行期間一次不漏的檢查則是能夠「商量」的事情。例如:數組下標是一個常量,如 foo[3],只要在編譯期根據數據流分析來肯定foo.length的值,並判斷下標「3」沒有越界,執行的時候就無需判斷了。
逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法中被定之後,它可能被外部方法所引用,例如做爲調用參數傳遞到其餘方法中,稱爲方法逃逸。甚至還有其可能被外部線程訪問到,譬如賦值給類變量或能夠在其餘線程中訪問的實例變量,稱爲線程逃逸!
參考資料:《深刻了解Java虛擬機》
CSDN博客連接:https://blog.csdn.net/u012834750/article/details/79488572