郭書超
(九江學院電子工程學院,江西 九江 332005)
即時編譯器編譯性能的好壞及代碼優化程度的高低作為衡量商用java虛擬機的關鍵技術指標,同時也是虛擬機技術水平的最好體現。由于java虛擬機規范知識規定了字節碼指令的動作,但并沒有規定虛擬機的實現方式。執行引擎的核心動作就是不停讀取字節碼,解釋(編譯)執行,直到虛擬機進程的退出為止。Sun HotSpot虛擬機執行引擎為解釋器與編譯器共存的架構方式,內部的編譯器是即時編譯器主要由Client Compiler和Server Compiler構成,解釋器與其中的一種構成混合模式的虛擬機執行引擎。
HotSpot的執行引擎采用解釋器和即時編譯器共存的架構,對于一般的代碼采用解釋器每次讀取字節碼指令,將指令解釋乘本地代碼并予以執行。這樣機制能夠有效節約內存,減少編譯時間,讓代碼更加快速的進入執行狀態,但是存在代碼執行效率低的缺點。即時編譯器采用熱點代碼偵測技術,實時把熱點代碼編譯成本地代碼,調用的時候優先使用本地編譯過的代碼,可以大大提高虛擬機的運行速度。另外不同的編譯器,還能有效實現局部或全局的代碼的優化,有效提高字節碼的解釋效率,節約程序的調用時間。
從java虛擬機角度觀察,hotspot中類的加載分兩種情況:一種是啟動類加載器的加載器,由CPP代碼實現;另外一種就是加載其他類的加載器。以下代碼分析的都是在目錄:/openjdk/hotspot/src/share/vm下,以下出現的目錄都位于該目錄之下。由于最開始java環境還沒有,通過CPP代碼構建編譯的環境:
首先:hotspot啟動時,根據運行環境的不同,決定使用的寄存器、指令集及緩存大小等,判斷CPU架構類型,在sparc、x86、x86-64或arm等結構中選擇,根據架構的不同加載不同的文件。
然后:進行加載過程的第一步—驗證:
(1)格式的驗證,主要驗證文件的魔數是否正確、主次版本號是否合理、常量池中的常量內類是否合法、常量的索引是否符合、結構是否符合UTF8編碼等。此時,如果常量池中的還有內容沒有加載,便進行常量池的清理就會出現錯誤。
(2)元數據驗證,主要是對字節碼描述信息的語義進行分析,使得符合java語言的規范,主要包括類是否有繼承,繼承的父類是否能夠被繼承,該類是否為抽象類,類中的字段是否與父類的沖突等。
(3)字節碼驗證,主要是驗證數據流和控制零分析,保證程序語義的正確,邏輯合理,實現虛擬機的安全運行。
(4)符號引用的驗證,主要是解析階段進行,對類的匹配信息驗證。驗證階段也是非常重要的,若出現錯誤,根據不同的時段,會拋出不同的異常。
接著:使用類加載器實現類的加載,類加載器通過類的全限定名將描述該類的二進制字節流放置到java虛擬機。類加載器的和類本身都需要在虛擬機中是唯一存在的,每個加載器擁有自己的類命名空間。類加載過程中,如果發現制定的包已經被虛擬機加載,就根據加載信息直接使用加載過的包,同時對類調用的計數器值加1。同樣的類加載器,結合不同的類加載,同樣可以在虛擬機中存在,通過哈希算法,被標識成不同的值。類加載過程中主要是采用雙親委派模型,通過啟動類加載器、擴展類加載器、應用程序加載器的共同配合進行加載。這種加載模式中,假設除了最頂層的類加載器外,其他的類都有父類加載器。在收到類加載請求之后,并不直接進行類的加載,將類加載的任務委派給父類加載器完成,由于每個類都是這樣進行,所有的類加載請求都會被提交到Objcet的類加載,只有當父類無法加載時,子類才嘗試自己加載類。
然后:虛擬機的運行。HotSpot虛擬機和主流的商用虛擬機一樣都是采用解釋器與編譯器共存的架構。這種架構的優勢體現在以下三個方面:
(1)在類剛加載時,首先工作在第0級,通過編譯策略決定java方法的編譯等級。此時主要由解釋器對類解釋執行,實現節約編譯時間,達到立即執行的目標。隨著類運行時間的累計,越來越多的代碼都會被標記為熱點代碼,經編譯器編譯成本地代碼,實現執行效率的提高。
(2)在代碼提交編譯到編譯成功投入運行的時段中,代碼的執行依舊靠解釋器予以解釋執行。
(3)在代碼優化過程中,若是出現了優化失敗的情況時,可以通過逆優化實現“代碼逃逸”,解釋器在此過程中充當著“逃逸門”的作用。在HotSpot虛擬機中使用不同的參數控制使用不同的即時編譯器,將解釋器和選定的即時編譯器搭配使用是其工作的常態,使用“-Xint”參數實現虛擬機在解釋模式下運行,老版本虛擬機可以通過參數“-Xcomp”強迫運行在編譯方式中。
為了平衡程序啟動的速度和運行效率,虛擬機采用了分層編譯的手段達到兩種編譯器共同參與編譯的目標。分層編譯的核心是編譯隊列的應用,對與隊列中的每個方法,JVM計算時間時間的發生率,每次出隊的都是發生率最大的元素,使得過時的方法很快就可以刪除掉。在解釋器解釋執行代碼時,當虛擬機偵測到某個方法或代碼塊(主要是循環)執行非常頻繁時,頻繁程度主要采用基于采樣的熱點探測和基于計數器的熱點探測兩種方法來裁決,前者實現簡單,容易受到外界影響,使用場合不多;后者結果更加準確,通過方法調用計數器和回邊計數器的共同配合,實現熱點代碼的探測。
經過熱點代碼的認定之后,熱點代碼被調用時,虛擬機就會檢查是否有被JIT編譯的版本,存在就會優先使用編譯后的代碼運行;否則將方法調用計數器或回邊計數器加上1,判斷方法調用計數器和回邊計數器的和是否超過計數器設定的閾值,如果超過閾值,就向即時編譯器提交該方法的代碼編譯請求,在等待編譯的時段內的代碼繼續以解釋的方式執行。引入熱點代碼是為了提高熱點代碼的執行效率,運行時,虛擬機會將這些代碼編譯成與平臺相關的機器碼,將抽象的IR(中間表示)、CFG(控制流圖)和SSA(靜態單賦值)轉變為具體的寄存器、編譯目標內容,達到縮短編譯時間實現代碼優化的目標。
經過前期的準備工作,編譯器選擇java方法或循環體作為編譯的目標。編譯方法時,首先創建一個Compilation類,該類中的方法compile_mothod()被用來執行編譯的過程,具體代碼c1c1_compiler.cpp。明確將編譯過程分成多個中間環節,甚至能夠通過VM選項,得到非常詳細的編譯細節。打開VM選項后,可以得到CFG文件,該文件描述了編譯的各個環節。
(1)生成HIR環節,HIR相當于基本塊組成的控制流圖。
(2)生成LIR環節,該環節中,編譯器生成了寄存器分配前的LIR代碼,相對與HIR環節,此處增加了LIR指令信息,局部變量的狀態也發生了變化,變量名分配了虛擬寄存器。該處的寄存器是LIR格式的虛擬寄存器,明確了機器指令,甚至包括指令名稱與尋址方式,通過分配物理寄存器明確實際地址即可。
(3)寄存器分配中為了充分利用寄存器資源,盡可能將程序變量盡量分配到寄存器中,達到提高執行速度的目標。如何將數據盡量長時間的保存在寄存器中,并將廢棄的數據盡快清除是一個必須解決的問題。HotSpot使用了線性掃描算法,該算法的核心是:對任意兩個變量的生命區間存在著重疊區域,不能將同一物理寄存器分配給這兩個變量。HashSet.add()方法完成寄存器的分配任務。最后生成優化后的字節碼。
經過經典優化如無用代碼消除、循環展開、循環表達式外提、消除公共子表達式、塊重排、常量傳播等優化后的代碼性能幾乎可以達到GNU C++編譯器的-O2參數的優化強度,說明基于熱點探測的即時觸發技術還是非常有效的優化手段。
本文通過研究HotSpot虛擬機類加載及優化的原理與代碼實現,在深刻理解其工作原理基礎上,加上對HotSpot代碼的閱讀,為自己理解虛擬機的工作原理與將來實現虛擬機打下良好的基礎。
[1]陳濤著.HotSpot實戰 [M].人民郵電出版社,2014(03).
[2]周志明著.深入理解Java虛擬機-JVM高級特性與最佳實踐 [M].機械工業出版社,2014(04).
[3]Tim Lindholm、 Frank Yellin、Gilad Bracha、Alex Buckley著,周志明,薛笛,吳璞淵,冶秀剛 譯 Java虛擬機規范(Java SE 7版)[M].機械工業出版社,2014(01).