在部分商用虛擬機(Sun HotSpot、IBM J9)中,Java 程序最初是經過解釋器進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定爲「熱點代碼」。爲了提升熱點代碼的執行效率,在運行時,虛擬機會把這些代碼編譯成本地機器碼,並進行各類層次的優化。完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,簡稱 JIT 編譯器)。前端
Java 虛擬機規範並無規定必需要有即時編譯器存在,更沒有限定或指導即時編譯器如何去實現。因此即時編譯器的功能徹底與虛擬機的具體實現相關。java
許多主流的商用虛擬機(如 HotSpot、J9),都採用解釋器與編譯器並存的架構。程序員
當程序須要迅速啓動和執行時,解釋器能夠首先發揮做用,省去編譯的時間,當即執行。在程序運行後,隨着時間的推移,編譯器把愈來愈多的代碼編譯成本地代碼後,能夠獲取更高的執行效率。算法
當程序運行環境中內存資源限制較大(如部分嵌入式系統),可使用解釋執行節約內存,反之可使用編譯執行提高效率。後端
解釋器能夠做爲編譯器激進優化時的一個「逃生門」,讓編譯器根據機率選擇一些大多數時候都能提高運行速度的激進優化手段,當激進優化不成立時,能夠經過逆優化退回到解釋狀態繼續執行。數組
HopSpot 虛擬機內置了兩個即時編譯器,分別稱爲 Client Compiler(C1 編譯器)和 Server Compiler(C2 編譯器)。默認採用解釋器與其中一個編譯器配合的方式工做,程序使用哪一個編譯器,取決於虛擬機是以 Client 模式仍是 Server 模式運行。虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可使用「-client」或「-server」參數強制指定虛擬機的運行模式。緩存
爲了在程序啓動響應速度與運行效率之間達到最佳平衡,HotSpot 虛擬機會逐漸啓用分層編譯的策略。架構
分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不一樣的編譯層次,其中包括:性能
實施分層編譯後,C一、C2 編譯器將會同時工做,用 C1 編譯器獲取更高的編譯速度,用 C2 編譯器獲取更好的編譯質量,解釋執行時也無須再承擔收集性能監控信息的任務。優化
熱點代碼:
兩種熱點代碼的編譯對象都是整個方法。第一種熱點代碼的編譯,因爲是由方法調用觸發的,理所固然會以整個方法做爲編譯對象。第二種熱點代碼的編譯,儘管是由循環體觸發的,但編譯器仍會以整個方法(而不是單獨的循環體)做爲編譯對象。
棧上替換:
被屢次執行的循環體成爲熱點代碼時,所觸發的編譯。由於編譯發生在方法執行過程當中,所以稱之爲棧上替換(也稱 OSR 編譯),即方法棧幀還在棧上,方法就被替換了。
判斷一段代碼是否是熱點代碼,是否是須要觸發即時編譯,這樣的行爲稱爲熱點探測。
熱點探測方式:
HotSpot 虛擬機使用的是基於計數器的熱點探測方法,它爲每一個方法準備了兩類計數器:
方法調用計數器觸發即時編譯:
回邊計數器觸發即時編譯:
默認設置下,不管是方法調用產生的即時編譯請求,仍是 OSR 編譯請求,虛擬機在代碼編譯還未完成以前,仍會按照解釋方式繼續執行,而編譯動做則在後臺的編譯線程中進行。
C1 編譯器編譯過程:
若是一個表達式 E 已經計算過了,而且從先前計算到如今 E 中全部變量的值都沒有變化,那麼 E 的此次出現就成了公共子表達式。對於這種表達式,沒有必要再次進行計算,直接用前面計算過的表達式結果代替 E 便可。
Java 語言訪問數組元素時,虛擬機系統會自動進行上下界的範圍檢查,一旦訪問超出範圍,將拋出一個運行時異常:java.lang.ArrayIndexOutOfBoundsException。
數組邊界檢查使得程序員即使沒有專門編寫防護代碼,也能夠避免大部分的溢出攻擊。但對於虛擬機的執行子系統來講,每次數組元素的讀寫都帶有一次隱含的條件斷定操做,若是程序中擁有大量數組訪問代碼,無疑大大增長了性能負擔。
編譯器能夠經過數據流分析斷定數組下標是否會越界,若是分析後肯定不會越界,那麼能夠把數組的上下界檢查消除。
把目標方法的代碼「複製」到發起調用的方法之中,避免發生真實的方法調用。
對於一個虛方法,編譯期作內聯的時候根本沒法肯定應該使用哪一個方法版本,爲了解決這個問題,引入了類型繼承關係分析(Class Hierarchy Analysis,CHA)技術。CHA 用於肯定在目前已加載的類中,某個接口是否有多於一種的實現,某個類是否存在子類、子類是否爲抽象類等信息。
守護內聯:
當虛方法只有一個目標版本時,也能夠進行內聯,但這種內聯屬於激進優化,須要預留一個「逃生門」,這種內聯稱爲守護內聯。進行守護內聯時,若是後續執行過程當中,加載了致使繼承關係發生變化的新類,則須要拋棄已經編譯的代碼,退回到解釋狀態執行,或者從新進行編譯。
內聯緩存:
內聯緩存是一個創建在目標方法正常入口以前的緩存。它的工做原理是:在未發生方法調用前,內聯緩存狀態爲空,第一次調用發生後,緩存記錄下方法接收者的版本信息,而且每次進行方法調用時都會比較接收者版本。若是接收者版本一致,那麼這個內聯還能夠用下去,若是不一致,說明程序使用了虛方法的多態特性,此時會取消內聯,查找虛方法表進行方法分派。
逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法中定義後,若是它被外部方法所引用或被外部線程訪問到,那麼就說這個對象發生了逃逸。
若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何途徑訪問到這個對象,則可能爲這個變量進行一些高效的優化。
若是肯定一個對象不會逃逸出方法以外,那麼可讓這個對象在棧上分配內存。這樣對象所佔用的內存空間就能夠隨棧幀出棧而銷燬,從而減小了垃圾收集系統的壓力。
若是肯定一個變量不會逃逸出線程,那麼這個變量的讀寫確定不會有競爭,所以能夠消除掉這個變量的線程同步措施。
若是肯定一個對象不會被外部訪問,而且這個對象能夠被拆散的話,那麼程序真正執行時可能不建立這個對象,而改成建立它的若干個被這個方法使用到的成員變量來代替,這個過程稱爲標量替換。
將對象拆分後,除了可讓對象的成員變量在棧上分配和讀寫以外,還能夠爲後續進一步的優化手段建立條件。
標量與聚合量: