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