閒談java中的程序編譯與優化技術

java中的程序編譯和優化技術同其他語言一樣基本都發生在編譯期。java的編譯期可根據不同的編譯器分爲三個部分,一個是前端編譯器,比如javac;它的工作就是把.java文件轉化爲.class文件。另一個是即時編譯器,比如JIT編譯器;它的工作是把.class文件中的某些熱點字節碼轉化爲本地機器碼,提高程序運行速度。最後一個是靜態提前編譯器,比如AOT靜態編譯器。它跳過了.class文件的生成的過程,直接把.java文件轉化爲本地機器碼。在某種意義上來說,這種方法已經放棄了java語言的平臺無關性,無法“一次編譯,到處運行”。前兩個編譯器分別代表了java語言的兩個編譯階段,接下來我們來看看這兩個階段。

每一個編譯器都由兩個部分組成,一個是它本身就要完成的任務——編譯代碼,另一個則是爲了優化程序而附帶的功能——優化技術。前端編譯器主要的優化工作針對於程序編碼,它注重於方便程序員的開發和增強代碼的可讀性。而即時編譯器的優化工作纔是真正意義上的優化,它針對程序運行,注重於提高程序的運行效率。至於爲何要把所有的程序運行優化技術都放在即時編譯器中,一個很重要的原因就是java語言的平臺無關性更確切地說應該是字節碼(.class文件)的平臺無關性。而能夠產生.class文件的語言遠不止java,包括Ruby等都可以。爲了讓這些語言也能夠享受優化技術,研發人員就把提高程序運行效率的優化措施放到了即時編譯器中。

前端編譯器,它的編譯過程主要由三個部分組成:解析和填充符號表、插入式註解處理器的註解處理過程以及語義分析和字節碼生成過程。首先我們先來看一下解析和填充符號表的過程,它包括語法分析、詞法分析和填充符號表三個部分。語法分析主要是將源代碼的字符流變成標記(Token)集合。詞法分析則是根據語法分析構建抽象語法樹的過程。最後的符號表則是把符號引用的符號地址一一對應並保存起來。符號表是給符號引用分配地址的依據。插入式註解處理器可以讀取、修改和添加抽象語法樹的所有元素。如果抽象語法樹的內容被修改了,那麼編譯要重新回到解析和填充符號表的階段,直到所有的插入式註解處理器不再對抽象語法樹進行修改爲止。最後一個過程是語義分析和字節碼生成過程。它包括標註檢查、數據及控制流分析、解語法糖以及字節碼生成四個階段。

前端編譯器的編碼優化技術主要是通過java中的語法糖來實現的。java語法糖主要有:泛型和類型擦除、自動裝箱拆箱、遍歷循環和條件編譯。其中java語言中的泛型屬於僞泛型,它是基於類型擦除的方法實現的。也就是在經過javac編譯後,泛型就會被還原成相應的具體的數據類型。而基於類型膨脹技術實現的泛型稱爲真實泛型。真實泛型在系統運行期生成,有自己的虛方法表和數據類型。也就是在java中List<Integer>和List<String>在編譯後都會變成一樣的原生類型List<E>。

即時編譯器,它的編譯過程主要包括一下幾個部分。它發生作用的時間是在程序運行過程中,程序的啓動是通過字節碼解釋器進行的。它的編譯對象是被多次調用的方法以及被多次執行的循環體。即時編譯器採用了分層編譯的思想,包括第0層編譯,字節碼執行,可觸發第1層編譯;第1層編譯,也成爲C1編譯,會把熱點代碼轉化爲本地機器碼,進行簡單可靠的優化;第2層編譯,除了把熱點代碼轉化爲本地機器碼之後,還會進行其他編譯時間較長的優化措施和激進優化。當某一段代碼成爲熱點代碼時,就會觸發即時編譯,我們常通過熱點探測技術來檢測一段代碼是否是熱點代碼。熱點探測主要有兩種,一種是基於採樣的熱點探測,它通過週期性地檢查棧頂,如果某種方法常常出現在棧頂,就認爲它是熱點方法。另一種是基於計數器的熱點探測。它需要爲每個方法建立並維護計數器,當計數器的值超過當前閾值時,這個方法就會變成熱點代碼。JVM首先會檢查當前方法是否存在本地機器碼,如果有就直接執行本地機器碼。如果沒有,就對其進行即時編譯。JVM會把即時編譯的過程放到後臺執行,當前程序先用熱點方法的字節碼繼續運行。等到即時編譯完成,再去用本地機器碼。即時編譯根據不同的運行環境可分爲Server Compiler和Client Compiler。其中Client Compiler採取的優化程度主要是C1級別的優化,它只會進行局部性的優化,耗時短。而Server Compiler採取的優化成都主要是C2級別的優化,它會進行幾乎所有經典的優化措施。

即時編譯器的優化措施有很多,包括公共子表達式消除、數組邊界檢查消除、方法內聯和逃逸分析。數組邊界檢查消除屬於一種比較激進的優化措施,在數組溢出情況較少的時候能夠提高不少的效率。但是如果數組溢出情況過多,反而會降低程序運行效率。方法內聯除了能夠消除方法調用的成本,更重要的是它能夠爲其它優化措施提供一個良好的代碼基礎。最後的逃逸分析是一種優化思路,不是具體的優化措施。代碼逃逸主要主要分析對象的動態作用域。分爲兩種,一種是方法逃逸,也就是某個對象會被其他方法調用到,另一種是線程逃逸,也就是某個對象會被其他線程調用到。基於逃逸分析的優化思想,有以下三種優化措施。一個是棧上分配對象。如果某個對象不會被除當前方法以外的其他方法調用到,那麼我們就可以直接把當前對象分配到棧上。那麼這個對象就會隨着當前方法的結束而自動銷燬,如此一來可以減輕GC收集器不少的工作量。另一個是同步消除,如果某個變量不會被除當前線程以外的線程訪問到,那麼我們對這個變量所做的同步措施就沒有必要了,可以把這些同步措施消除掉。最後一個是標量替換。如果一個對象不會被外部訪問,並且這個對象可以被拆散的話,我們就可以不創建這個對象,轉而把這個對象中被當前方法訪問到的屬性用標量表示,並且分配到當前方法的棧上。由於逃逸技術在當前還不是很成熟,因此在很多虛擬機中都是默認不開啓。

 

該博文是本人閱讀完《深入理解Java虛擬機》後做的一個知識點整合,更注重知識的關聯性和完整性,因此不像其他博客一樣有大小標題。沒有JVM基礎的建議先去看我的另外兩篇博客《早期(編譯期)優化(筆記)》《晚期(運行期)優化(筆記)》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章