1. 程式人生 > >閒談java中的程式編譯與優化技術

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

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

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

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

前端編譯器的編碼優化技術主要是通過java中的語法糖來實現的。java語法糖主要有:泛型和型別擦除、自動裝箱拆箱、遍歷迴圈和條件編譯。其中java語言中的泛型屬於偽泛型,它是基於擦除法實現的。也就是在經過javac編譯後,泛型就會被還原成相應的具體的資料型別。而基於型別膨脹技術實現的泛型稱為真泛型。

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

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

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