Java語言的編譯期是一段操做過程,具體的能夠分爲三類:前端
因此編譯期優化是上者三者共同作出的貢獻。java
源代碼,字節碼,機器碼,本地代碼?編程
源文件就是.java文件。字節碼就是.class文件。機器碼和本地代碼是計算機可以直接識別運行的代碼,就是機器指令。衆所周知,java的特色之一就是跨平臺性,跨平臺的結果是運行效率慢,JVM爲了增快速度,將某些代碼會編譯成機器碼,以此提升運行效率。後端
我以前作過一篇關於javac編譯器的博客,較爲詳細的講了Javac編譯的各步驟及做用,若是看過那篇文章的小夥伴們就知道Javac對代碼的運行效率幾乎沒有優化措施,可是,有一些「語法糖」是靠javac編譯器實現的,例如foreach語法、註解等。數組
那麼關於Javac編譯器的部分能夠去看以前的博客,這裏就不耽誤你們時間了。緩存
舉個JIT編譯器優化的例子,當虛擬機發現某個方法被頻繁運行時(或一個屢次執行的循環體),就會把這些代碼認定爲「熱點代碼」,爲提升效率,運行時,就會把這些代碼編譯成與本地平臺相關的機器碼,而完成這個任務的,就是JIT編譯器。安全
能夠經過java -verison來查看本身的JIT模式,如圖,個人是Server模式,而且採用的mixed mode。下面來解釋一下什麼是mixed mode和Server,先來分析mixed mode。 bash
HotSpot虛擬機採用解釋器和編譯器的架構。架構
這裏的解釋器做用就是將字節碼一條一條翻譯爲機器碼,它的特色是當即執行,節約內存函數
它的做用是把源程序的每一條語句都編譯成機器語言,並保存爲二進制文件。
那麼爲何要同時使用解釋器和編譯器呢?由於解釋器很慢,但節約內存;而編譯器編譯成本地代碼後執行效率更高。
還有一點,當編譯器使用激進優化不成立時(即優化事後發現並無起到優化做用),例如加載了新類後繼承結構變化,這時能夠逆優化退回到解釋狀態繼續執行。那麼這裏的mixed mode也就是解釋器和編譯器組合的混合模式了。
JIT編譯器能夠分爲兩類,Client和Servcer又名C1,C2。
C1編譯器將字節碼編譯爲本地代碼,進行簡單可靠的優化。C2編譯器則會啓動一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
在jdk1.8以後,引入了分層編譯的策略,在運行初期開啓C1編譯器編譯,隨時間的推移執行頻率高的代碼會再次被C2編譯器編譯:
JIT編譯的熱點代碼有兩類
那麼這個屢次是多少次呢?這就須要進行熱點探測。目前主要的熱點探測方式有如下兩種:
採用這種方法的虛擬機會週期性地檢查各個線程的棧頂,若是發現某些方法常常出如今棧頂,那這個方法就是「熱點方法」。這種探測方法的好處是實現簡單高效,還能夠很容易地獲取方法調用關係(將調用堆棧展開便可),缺點是很難精確地確認一個方法的熱度,容易由於受到線程阻塞或別的外界因素的影響而擾亂熱點探測。
採用這種方法的虛擬機會爲每一個方法(甚至是代碼塊)創建計數器,統計方法的執行次數,若是執行次數超過必定的閥值,就認爲它是「熱點方法」。這種統計方法實現複雜一些,須要爲每一個方法創建並維護計數器,並且不能直接獲取到方法的調用關係,可是它的統計結果相對更加精確嚴謹。
HotSpot採用基於計數器的熱點探測方法,他爲每一個方法準備了方法調用計數器和回邊計數器:
方法調用計數器統計的並非方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間內方法被調用的次數。當超過必定的時間限度,若是方法調用次數仍然不足以讓它提交給即時編譯器,那這個方法的調用計數器就會衰減通常,這個過程稱爲方法調用計數器熱度的衰減。
回邊計數器統計的是一個方法中循環體代碼執行的次數,它沒有熱度衰減。創建回邊計數器統計的目的是爲了處罰OSR編譯,OSR即棧上替換,也就是編譯發生在方法執行過程之中。
OSR編譯:某段循環執行的代碼還在不停的循環中,若是在某次循環以後,計數器達到了某一閾值,這時JVM已經認定這段代碼是熱點代碼,此時編譯器會將這段代碼編譯成機器語言並緩存後,可是這段循環仍在執行,JVM就會把執行代碼替換掉,那麼等循環到下一次時,就會直接執行緩存的編譯代碼,而不須要必須等到循環結束才進行替換,這個就是所謂的棧上替換
下面來看一下JIT生成代碼時的代碼優化技術:
他的意思是若是一個表達式E已經計算過了,而且從先前的計算到如今E中全部變量的值都沒有發生變化,那麼E的此次出現就成爲了公共子表達式。
int d = (c * b) * 12 + a + (a + b * c);
//優化爲:
int d = E * 12 + a + (a+E);
複製代碼
咱們知道Java是動態安全的語言,若是訪問一個超出數組邊界的元素會拋出異常,若是沒有優化,那麼每一次對數組元素的讀寫都會進行判斷是否越界,這顯然是很消耗性能的。但毫無疑問的是數組邊界檢查是必須作的。
//1.若是數組下標是常量
array[3]
//編譯器根據數據流分析肯定foo.length的值,並判斷下標"3"沒有越界,執行的時候就無須判斷了
//2.數組下標是循環變量
for(int i...)
array[i]
/**若是編譯器經過數據流分析就能夠斷定循環變量"0<=i< foo.length",
那在整個循環中就能夠把數組的上下界檢查消除掉,這能夠節省不少次的條件判斷操做。*/
複製代碼
方法內聯能夠理解爲將目標方法的代碼「複製」到發起調用的方法之中,避免真實的方法調用。但實際上,咱們平時所說的面向接口編程,會使用多態等特性,而多態是要在運行時才能斷定到底使用哪一個方法的(實際上Java的默認實例方法是虛方法,而虛方法作內聯時根本沒法肯定使用哪一個版本),因此咱們就能夠知道要達到方法內聯的條件是比較苛刻的。
那麼,方法內聯以後可以進行哪些優化呢?
下面舉個例子:
//內聯前的代碼
static class B{
int value;
final int get(){
return value;
}
}
public void foo(){
y = b.get();
z = b.get();
sum = y + z;
}
複製代碼
//內聯後的代碼
public void foo(){
y = b.value;
z = b.value;
sum = y + z;
}
複製代碼
內聯後採起的其餘優化
//冗餘訪問消除
public void foo(){
y = b.value;
z = y;
sum = y + z;
}
複製代碼
//複寫傳播
public void foo(){
y = b.value;
y = y;
sum = y + y;
}
複製代碼
//無用代碼消除
public void foo(){
y = b.value;
sum = y + y;
}
複製代碼
從字節碼的角度來看的話,只有使用invokespecial指令調用的私有方法、實力構造器、父類方法;以及invokestatic指令進行調用的靜態方法纔是在編譯期進行解析的;另外final修飾的方法也是非虛方法。前文也說了虛方法沒法在編譯期肯定調用的具體方法,因此爲了解決虛方法的內聯問題,JVM設計師們引入了一個「類型繼承關係分析」(CHA)的技術。
內聯步驟:
因此內聯優化不少狀況下是激進優化,須要有「逃生門」回到解釋狀態從新執行
它的基本行爲就是分析對象狀態做用域:例如若是在一個方法內返回了一個方法內生成的新對象,若是被引用,這是方法逃逸;若是被外部線程訪問到,這是線程逃逸。
那麼JIT又是如何對逃逸分析進行優化的呢?
事實上,逃逸分析還未成熟,緣由也很簡單,若是要判斷一個對象是否會逃逸,須要進行數據流的一系列分析,而這個逃逸分析帶來的消耗未必比逃逸分析帶來的優化小。
因此,綜上所述,JIT編譯器做用時間在程序運行時,做用將運行頻繁的代碼編譯爲本地代碼,因此由於JIT做用時間在運行時,因此他在優化性能的同時也讓java保持了跨平臺的特性
AOT是jdk9才引入的編譯方式,和JIT不一樣,AOT是在程序運行前進行的靜態編譯,那麼爲何有了JIT後還須要AOT呢?
彷佛有一個問題,若是AOT的出現是由於JIT優化的時間在運行時,那麼爲何不直接在javac編譯階段優化呢,或者爲何不在編譯階段就優化完畢呢?
但事實上也有在靜態編譯期間將優化全作完的,例如Excelsior JET,意思就是能夠指定編譯模式爲不使用動態特性,那若是發生運行時類加載了一個新類,那麼就直接回退到從新編譯JIT就行了。因此,爲了java的動態性的實現,咱們很難在靜態編譯期就徹底實現優化。
參考:《深刻理解Java虛擬機》《深刻分析JavaWeb》《逃逸分析爲何不在編譯時間運行》 https://www.zhihu.com/question/27963717/answer/38871719