微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)
java語言的編譯期實際上是一段不肯定的操做過程,由於它能夠分爲三類編譯過程:
1.前端編譯:把前端
早期編譯過程主要分爲3個部分:
1.解析與填充符號表過程:詞法、語法分析;填充符號表
2.插入式註解處理器的註解處理過程
3.語義分析與字節碼生成過程:標註檢查、數據與控制流分析、解語法糖、字節碼生成複製代碼
Java語言中的泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換成原來的原生類型了,而且在相應的地方插入了強制轉型代碼java
泛型擦除前的例子
public static void main( String[] args )
{
Map<String,String> map = new HashMap<String, String>();
map.put("hello","你好");
System.out.println(map.get("hello"));
}
泛型擦除後的例子
public static void main( String[] args )
{
Map map = new HashMap();
map.put("hello","你好");
System.out.println((String)map.get("hello"));
}複製代碼
自動裝箱、拆箱在編譯以後會被轉化成對應的包裝和還原方法,如Integer.valueOf()與Integer.intValue(),而遍歷循環則把代碼還原成了迭代器的實現,變長參數會變成數組類型的參數。
然而包裝類的「==」運算在不遇到算術運算的狀況下不會自動拆箱,以及它們的equals()方法不處理數據轉型的關係。程序員
Java語言也能夠進行條件編譯,方法就是使用條件爲常量的if語句,它在編譯階段就會被「運行」:面試
public static void main(String[] args) {
if(true){
System.out.println("block 1");
}
else{
System.out.println("block 2");
}
}
編譯後Class文件的反編譯結果:
public static void main(String[] args) {
System.out.println("block 1");
}複製代碼
只能是條件爲常量的if語句,這也是Java語言的語法糖,根據布爾常量值的真假,編譯器會把分支中不成立的代碼塊消除掉數據庫
Java程序最初是經過解釋器進行解釋執行的,當程序須要迅速啓動和執行時,解釋器能夠首先發揮做用,省去編譯時間,當即執行;當程序運行後,隨着時間的推移,編譯期逐漸發揮做用,把愈來愈多的代碼編譯成本地代碼,得到更高的執行效率。解釋執行節約內存,編譯執行提高效率。 同時,解釋器能夠做爲編譯器激進優化時的一個「逃生門」,讓編譯器根據機率選擇一些大多數時候都能提高運行速度的優化手段,當激進優化的假設不成立,則經過逆優化退回到解釋狀態繼續執行。後端
HotSpot虛擬機中內置了兩個即時編譯器,分別稱爲Client Compiler(C1編譯器)和Server Compiler(C2編譯器),默認採用解釋器與其中一個編譯器直接配合的方式工做,使用哪一個編譯器取決於虛擬機運行的模式,也能夠本身去指定。若強制虛擬機運行與「解釋模式」,編譯器徹底不介入工做,若強制虛擬機運行於「編譯模式」,則優先採用編譯方式執行程序,解釋器仍然要在編譯沒法進行的狀況下介入執行過程。
數組
分層編譯策略做爲默認編譯策略在JDK1.7的Server模式虛擬機中被開啓,其中包括:
第0層:程序解釋執行,解釋器不開啓性能監控功能,可觸發第1層編譯;
第1層:C1編譯,將字節碼編譯成本地代碼,進行簡單可靠的優化,若有必要將加入性能監控的邏輯;
第2層:C2編譯,也是將字節碼編譯成本地代碼,可是會啓動一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
實施分層編譯後,C1和C2將會同時工做,C1獲取更高的編譯速度,C2獲取更好的編譯質量,在解釋執行的時候也無須再承擔性能監控信息的任務。 複製代碼
在運行過程當中會被即時編譯器編譯的「熱點代碼」有兩類:
1.被屢次調用的方法:由方法調用觸發的編譯,屬於JIT編譯方式
2.被屢次執行的循環體:也以整個方法做爲編譯對象,由於編譯發生在方法執行過程當中,所以成爲棧上替換(OSR編譯)
熱點探測斷定方式有兩種:
1.基於採樣的熱點探測:虛擬機週期性的檢查各個線程的棧頂,若是某個方法常常出如今棧頂,則斷定爲「熱點方法」。(簡單高效,能夠獲取方法的調用關係,但容易受線程阻塞或別的外界因素影響擾亂熱點探測)
2.基於計數的熱點探測:虛擬機爲每一個方法創建一個計數器,統計方法的執行次數,超過必定閾值就是「熱點方法」。(須要爲每一個方法維護計數器,不能直接獲取方法的調用關係,可是統計結果精確嚴謹) 複製代碼
HotSpot虛擬機使用的是第二種,它爲每一個方法準備了兩類計數器:方法調用計數器和回邊計數器,下圖表示方法調用計數器觸發即時編譯:
緩存
若是不作任何設置,執行引擎會繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成,下次調用纔會使用已編譯的版本。另外,方法調用計數器的值也不是一個絕對次數,而是一段時間以內被調用的次數,超過這個時間,次數就減半,這稱爲計數器熱度的衰減。安全
下圖表示回邊計數器觸發即時編譯:
bash
回邊計數器沒有計數器熱度衰減的過程,所以統計的就是絕對次數,而且當計數器溢出時,它還會把方法計數器的值也調整到溢出狀態,這樣下次進入該方法的時候就會執行標準編譯過程。
虛擬機設計團隊幾乎把對代碼的全部優化措施都集中在了即時編譯器之中,那麼在編譯器編譯的過程當中,到底作了些什麼事情呢?下面將介紹幾種最有表明性的優化技術:
公共子表達式消除
若是一個表達式E已經計算過了,而且先前的計算到如今E中全部變量的值都沒有發生變化,那麼E的此次出現就成爲了公共表達式,能夠直接用以前的結果替換。
例:int d = (c * b) * 12 + a + (a + b * c) => int d = E * 12 + a + (a + E)
數組邊界檢查消除
Java語言中訪問數組元素都要進行上下界的範圍檢查,每次讀寫都有一次條件斷定操做,這無疑是一種負擔。編譯器只要經過數據流分析就能夠斷定循環變量的取值範圍永遠在數組長度之內,那麼整個循環中就能夠把上下界檢查消除,這樣能夠省不少次的條件判斷操做。
另外一種方法叫作隱式異常處理,Java中空指針的判斷和算術運算中除數爲0的檢查都採用了這個思路:
if(foo != null){
return foo.value;
}else{
throw new NullPointException();
}
使用隱式異常優化之後:
try{
return foo.value;
}catch(segment_fault){
uncommon_trap();
}
當foo極少爲空時,隱式異常優化是值得的,可是foo常常爲空,這樣的優化反而會讓程序變慢,而HotSpot虛擬機會根據運行期收集到的Profile信息自動選擇最優方案。複製代碼
方法內聯
方法內聯能去除方法調用的成本,同時也爲其餘優化創建了良好的基礎,所以各類編譯器通常會把內聯優化放在優化序列的最靠前位置,然而因爲Java對象的方法默認都是虛方法,所以方法調用都須要在運行時進行多態選擇,爲了解決虛方法的內聯問題,首先引入了「類型繼承關係分析(CHA)」的技術。
1.在內聯時,如果非虛方法,則能夠直接內聯
2.遇到虛方法,首先根據CHA判斷此方法是否有多個目標版本,若只有一個,能夠直接內聯,可是須要預留一個「逃生門」,稱爲守護內聯,若在程序的後續執行過程當中,加載了致使繼承關係發生變化的新類,就須要拋棄已經編譯的代碼,退回到解釋狀態執行,或者從新編譯。
3.若CHA判斷此方法有多個目標版本,則編譯器會使用「內聯緩存」,第一次調用緩存記錄下方法接收者的版本信息,而且每次調用都比較版本,若一致則能夠一直使用,若不一致則取消內聯,查找虛方法表進行方法分派。複製代碼
逃逸分析
逃逸分析的基本行爲就是分析對象動態做用域,當一個對象被外部方法所引用,稱爲方法逃逸;當被外部線程訪問,稱爲線程逃逸。若能證實一個對象不會被外部方法或進程引用,則能夠爲這個變量進行一些優化:
1.棧上分配:若是肯定一個對象不會逃逸,則可讓它分配在棧上,對象所佔用的內存空間就能夠隨棧幀出棧而銷燬。這樣能夠減少垃圾收集系統的壓力。
2.同步消除:線程同步相對耗時,若是肯定一個變量不會逃逸出線程,那這個變量的讀寫不會有競爭,則對這個變量實施的同步措施也就能夠消除掉。
3.標量替換:若是逃逸分析證實一個對象不會被外部訪問,而且這個對象能夠被拆散的話,那麼程序真正執行的時候能夠不建立這個對象,改成直接建立它的成員變量,這樣就能夠在棧上分配。複製代碼
但是目前還不能保證逃逸分析的性能收益一定高於它的消耗,因此這項技術還不是很成熟。
Java虛擬機的即時編譯器與C/C++的靜態編譯器相比,可能會因爲下面的緣由致使輸出的本地代碼有一些劣勢:
1.即時編譯器運行佔用的是用戶程序的運行時間,具備很大的時間壓力,所以不敢隨便引入大規模的優化技術;
2.Java語言是動態的類型安全語言,虛擬器須要頻繁的進行動態檢查,如空指針,上下界範圍,繼承關係等;
3.Java中使用虛方法頻率遠高於C++,則須要進行多態選擇的頻率遠高於C++;
4.Java是能夠動態擴展的語言,運行時加載新的類可能改變原有的繼承關係,許多全局的優化措施只能以激進優化的方式來完成;
5.Java語言的對象內存都在堆上分配,垃圾回收的壓力比C++大
然而,Java語言這些性能上的劣勢換取了開發效率上的優點,而且因爲C++編譯器全部優化都是在編譯期完成的,以運行期性能監控爲基礎的優化措施都沒法進行,這也是Java編譯器獨有的優點。複製代碼
微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)