1. 程式人生 > >深入理解Java虛擬機器讀書筆記-java編譯期和執行期優化

深入理解Java虛擬機器讀書筆記-java編譯期和執行期優化

編譯期優化



解析和填充符號表

1.詞法、 語法分析

詞法分析是將原始碼的字元流轉變為標記(Token)集合,單個字元是程式編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、 變數名、 字面量、 運算子都可以成為標記。Token不可再拆分。語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract SyntaxTree,AST)是一種用來描述程式程式碼語法結構的樹形表示方式,語法樹的每一個節點都代表著程式程式碼中的一個語法結構(Construct),例如包、 型別、 修飾符、 運算子、 介面、 返回值甚至程式碼註釋等都可以是一個語法結構。

2.填充符號表

符號表(Symbol Table)是由一組符號地址和符號資訊構成的表格,讀者可以把它想象成雜湊表中K-V值對的形式(實際上符號
表不一定是雜湊表實現,可以是有序符號表、 樹狀符號表、 棧結構符號表等)。 
符號表中所登記的資訊在編譯的不同階段都要用到。 在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間程式碼。 在目的碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。

註解處理

在JDK 1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,我們可以把它看做是一組編譯器的外掛,在這些外掛裡面,可以讀取、 修改、 新增抽象語法樹中的任意元素。 如果這些外掛在處理註解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式註解處理器都沒有再對語法樹進行修改為止,每一次迴圈稱為一個Round。

語義分析與位元組碼生成

語義分析的主要任務是對結構上正確的源程式進行上下文有關性質的審查,如進行型別審查(是否合乎語義邏輯必須限定在具體的語言與具體的上下文環境之中才有意義)。

1.標註檢查

標註檢查步驟檢查的內容包括諸如變數使用前是否已被宣告、 變數與賦值之間的資料型別是否能夠匹配等。 在標註檢查步驟中,還有一個重要的動作稱為常量摺疊。

2.資料及控制流分析

資料及控制流分析是對程式上下文邏輯更進一步的驗證,它可以檢查出諸如程式區域性變數在使用前是否有賦值、 方法的每條路徑是否都有返回值、 是否所有的受查異常都被正確處理了等問題(將區域性變數宣告為final,對執行期是沒有影響的,變數的不變性僅僅由編譯器在編譯期間保障)。

3.解語法糖

語法糖可以看做是編譯器實現的一些“小把戲”,這些“小把戲”可能會使得效率“大提升”。

泛型與型別擦除

本質是引數化型別(Parametersized Type)的應用,也就是說所操作的資料型別被指定為一個引數。 這種引數型別可以用在類、 介面和方法的建立中,分別稱為泛型類、 泛型介面和泛型方法。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程式設計師去保障這項操作的正確性,許多ClassCastException的風險就會轉嫁到程式執行期之中。C#裡面泛型無論在程式原始碼中、 編譯後的IL中(Intermediate Language,中間語言,這時候泛型是一個佔位符),或是執行期的CLR中,都是切實存在的,List<int>與List<String>就是兩個不同的型別,它們在系統執行期生成,有自己的虛方法表和型別資料,這種實現稱為型別膨脹,基於這種方法實現的泛型稱為真實泛型。Java語言中的泛型則不一樣,它只在程式原始碼中存在,在編譯後的位元組碼檔案中,就已經替換為原來的原生型別(Raw Type,也稱為裸型別)了,並且在相應的地方插入了強制轉型程式碼,因此,對於執行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為型別擦除,基於這種方法實現的泛型稱為偽泛型。擦除法所謂的擦除,僅僅是對方法的Code屬性中的位元組碼進行擦除,實際上元資料中還是保留了泛型資訊,這也是我們能通過反射手段取得引數化型別的根本依據。

自動裝箱、拆箱、迴圈遍歷

自動裝箱、 拆箱在編譯之後被轉化成了對應的包裝和還原方法,如本例中的Integer.valueOf()與Integer.intValue()方法,而遍歷迴圈則把程式碼還原成了迭代器的實現,這也是為何遍歷迴圈需要被遍歷的類實現Iterable介面的原因。

條件編譯

C、 C++中使用前處理器指示符(#ifdef)來完成條件編譯。 C、 C++的前處理器最初的任務是解決編譯時的程式碼依賴關係(如非常常用的#include預處理命令)在Java語言之中並沒有使用前處理器,因為Java語言天然的編譯方式(編譯器並非一個個地編譯Java檔案,而是將所有編譯單元的語法樹頂級節點輸入到待處理列表後再進行編譯,因此各個檔案之間能夠互相提供符號資訊)無須使用前處理器。條件編譯的實現方式使用了if語句,所以它必須遵循最基本的Java語法,只能寫在方法體內部,因此它只能實現語句基本塊(Block)級別的條件編譯,而沒有辦法實現根據條件調整整個Java類的結構。

4.位元組碼生成

把前面各個步驟所生成的資訊(語法樹、 符號表)轉化成位元組碼寫到磁碟中,編譯器還進行了少量的程式碼新增和轉換工作(如把字串的加操作替換為StringBuffer或StringBuilder)。

執行期優化

Java程式最初是通過直譯器(Interpreter)進行解釋執行的,當虛擬機發現某個方法或程式碼塊的執行特別頻繁時,就會把這些程式碼認定為“熱點程式碼”(Hot Spot Code)。 為了提高熱點程式碼的執行效率,在執行時,虛擬機器將會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler,下文中簡稱JIT編譯器)。當程式執行環境中記憶體資源限制較大(如部分嵌入式系統中),可以使用解釋執行節約記憶體,反之可以使用編譯執行來提升效率。
使用者可以使用引數“-Xint”強制虛擬機器運行於“解釋模式”(Interpreted Mode),這時編譯器完全不介入工作,全部程式碼都使用解釋方式執行。使用引數“-Xcomp”強制虛擬機器運行於“編譯模式”(Compiled Mode),這時將優先採用編譯方式執行程式,但是直譯器仍然要在編譯無法進行的情況下介入執行過程。當一個方法被呼叫時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優先使用編譯後的原生代碼來執行。 如果不存在已被編譯過的版本,則將此方法的呼叫計數器值加1,然後判斷方法呼叫計數器與回邊計數器值之和是否超過方法呼叫計數器的閾值。 如果已超過閾值,那麼將會向即時編譯器提交一個該方法的程式碼編譯請求。如果不做任何設定,方法呼叫計數器統計的並不是方法被呼叫的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被呼叫的次數。 當超過一定的時間限度,如果方法的呼叫次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的呼叫計數器就會被減少一半,這個過程稱為方法呼叫計數器熱度的衰減(Counter Decay),而這段時間就稱為此方法統計的半衰週期(Counter Half Life Time)。 進行熱度衰減的動作是在虛擬機器進行垃圾收集時順便進行的,可以使用虛擬機器引數-XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法呼叫的絕對次數,這樣,只要系統執行時間足夠長,絕大部分方法都會被編譯成原生代碼。 另外,可以使用-XX:CounterHalfLifeTime引數設定半衰週期的時間,單位是秒。

Client Compiler


Server Compiler

幾乎能達到GNU C++編譯器使用-O2引數時的優化強度,它會執行所有經典的優化動作,如無用程式碼消除(Dead Code Elimination)、 迴圈展開(Loop Unrolling)、 迴圈表示式外提(Loop Expression Hoisting)、 消除公共子表示式(Common Subexpression Elimination)、 常量傳播(Constant Propagation)、 基本塊重排序(Basic Block Reordering)等,還會實施一些與Java語言特性密切相關的優化技術,如範圍檢查消除(Range Check Elimination)、 空值檢查消除(Null Check Elimination)
  • -XX:+PrintCompilation要求虛擬機器在即時編譯時將被編譯成原生代碼的方法名稱打印出來
  • -XX:+PrintInlining要求虛擬機器輸出方法內聯資訊
  • -XX:+PrintAssembly引數要求虛擬機器列印編譯方法的彙編程式碼
  • -XX:+PrintOptoAssembly(用於Server VM)或-XX:+PrintLIR(用於Client VM)來輸出比較接近最終結果的中間程式碼表示。

常用編譯優化技術

方法內聯(Method Inlining)一是去除方法呼叫的成本(如建立棧幀等),二是為其他優化建立良好的基礎。

冗餘訪問消除(Redundant Loads Elimination)

複寫傳播(Copy Propagation)

無用程式碼消除(Dead Code Elimination)

公共子表示式消除

如果一個表示式E已經計算過了,並且從先前的計算到現在E中所有變數的值都沒有發生變化,那麼E的這次出現就成為了公共子表示式。 對於這種表示式,沒有必要花時間再對它進行計算,只需要直接用前面計算過的表示式結果代替E就可以了。

陣列邊界檢查消除(Array Bounds Checking Elimination)

逃逸分析

如果確定一個物件不會逃逸出方法之外,那讓這個物件在棧上分配記憶體將會是一個很不錯的主意,物件所佔用的記憶體空間就可以隨棧幀出棧而銷燬。
  • -XX:+DoEscapeAnalysis手動開啟逃逸分析
  • -XX:+PrintEscapeAnalysis檢視分析結果
  • -XX:+EliminateAllocations開啟標量替換
  • -XX:+EliminateLocks來開啟同步消除
  • -XX:+PrintEliminateAllocations檢視標量