Java虛擬機(三)編譯子系統

編譯分爲三種:

  • .java文件轉變成.class文件的過程——前端編譯器(其實叫“編譯器的前端”更準確一些)
  • 把字節碼轉變成機器碼的過程——後端運行期編譯器(JIT編譯器,Just In Time Compiler)
  • 直接把*.java文件編譯成本地機器代碼的過程——靜態提前編譯器(AOT編譯器,Ahead Of Time Compiler)

前端編譯器:Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)。
JIT編譯器:HotSpot VM的C1、C2編譯器。
AOT編譯器:GNU Compiler for the Java(GCJ)、Excelsior JET。

在前端編譯器中,“優化”手段主要用於提升程序的編碼效率,之所以把Javac這類將Java代碼轉變爲字節碼的編譯器稱做“前端編譯器”,是因爲它只完成了從程序到抽象語法樹或中間字節碼的生成,而在此之後,還有一組內置於虛擬機內部的“後端編譯器”完成了從字節碼生成本地機器碼的過程,即前面多次提到的即時編譯器或JIT編譯器,這個編譯器的編譯速度及編譯結果的優劣,是衡量虛擬機性能一個很重要的指標。

早期(編譯期)優化

javac編譯器:

  • 解析與填充符號表
    1.詞法、語法分析
    詞法分析是將源代碼的字符流轉變爲標記(Token)集合,語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等都可以是一個語法結構。
    2.填充符號表
    符號表(Symbol Table)是由一組符號地址和符號信息構成的表格,讀者可以把它想象成哈希表中K-V值對的形式(實際上符號表不一定是哈希表實現,可以是有序符號表、樹狀符號表、棧結構符號表等)。符號表中所登記的信息在編譯的不同階段都要用到。
  • 註解處理器
    提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,我們可以把它看做是一組編譯器的插件,在這些插件裏面,可以讀取、修改、添加抽象語法樹中的任意元素。如果這些插件在處理註解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式註解處理器都沒有再對語法樹進行修改爲止,每一次循環稱爲一個Round,也就是圖10-4中的迴環過程。
  • 語義分析與字節碼生成
    語法分析之後,編譯器獲得了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查。語義分析過程分爲1.標註檢查以及2.數據及控制流分析兩個步驟。
    3.接着解語法糖。
    4.字節碼生成

晚期(運行期)優化

當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定爲“熱點代碼”(Hot Spot Code)。爲了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,下文中簡稱JIT編譯器)。

HotSpot虛擬機內的即時編譯器

爲何HotSpot虛擬機要使用解釋器與編譯器並存的架構?
爲何HotSpot虛擬機要實現兩個不同的即時編譯器?
程序何時使用解釋器執行?何時使用編譯器執行?
哪些程序代碼會被編譯爲本地代碼?如何編譯爲本地代碼?
如何從外部觀察即時編譯器的編譯過程和編譯結果?

解釋器與編譯器:
當程序需要迅速啓動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程序運行後,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以獲取更高的執行效率。當程序運行環境中內存資源限制較大(如部分嵌入式系統中),可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率。

HotSpot虛擬機中內置了兩個即時編譯器,分別稱爲Client Compiler和Server Compiler,或者簡稱爲C1編譯器和C2編譯器(也叫Opto編譯器)。用Client Compiler獲取更高的編譯速度,用Server Compiler來獲取更好的編譯質量

熱點代碼:

  • 被多次調用的方法(棧上替換)
  • 被多次執行的循環體

熱點探測:

  • 基於採樣的熱點探測
  • 基於計數器的熱點探測
    • 方法調用計數器(Invocation Counter)
    • 回邊計數器(Back Edge Counter)

編譯優化技術

(待完善)

Java與C/C++編譯器對比

Java虛擬機的即時編譯器與C/C++的靜態優化編譯器相比,可能會由於下列這些原因而導致輸出的本地代碼有一些劣勢(下面列舉的也包括一些虛擬機執行子系統的性能劣勢):
第一,因爲即時編譯器運行佔用的是用戶程序的運行時間,具有很大的時間壓力,它能提供的優化手段也嚴重受制於編譯成本。如果編譯速度不能達到要求,那用戶將在啓動程序或程序的某部分察覺到重大延遲,這點使得即時編譯器不敢隨便引入大規模的優化技術,而編譯的時間成本在靜態優化編譯器中並不是主要的關注點。
第二,Java語言是動態的類型安全語言,這就意味着需要由虛擬機來確保程序不會違反語言語義或訪問非結構化內存。從實現層面上看,這就意味着虛擬機必須頻繁地進行動態檢查,如實例方法訪問時檢查空指針、數組元素訪問時檢查上下界範圍、類型轉換時檢查繼承關係等。對於這類程序代碼沒有明確寫出的檢查行爲,儘管編譯器會努力進行優化,但是總體上仍然要消耗不少的運行時間。
第三,Java語言中雖然沒有virtual關鍵字,但是使用虛方法的頻率卻遠遠大於C/C++語言,這意味着運行時對方法接收者進行多態選擇的頻率要遠遠大於C/C++語言,也意味着即時編譯器在進行一些優化(如前面提到的方法內聯)時的難度要遠大於C/C++的靜態優化編譯器。
第四,Java語言是可以動態擴展的語言,運行時加載新的類可能改變程序類型的繼承關係,這使得很多全局的優化都難以進行,因爲編譯器無法看見程序的全貌,許多全局的優化措施都只能以激進優化的方式來完成,編譯器不得不時刻注意並隨着類型的變化而在運行時撤銷或重新進行一些優化。
第五,Java語言中對象的內存分配都是堆上進行的,只有方法中的局部變量才能在棧上分配。而C/C++的對象則有多種內存分配方式,既可能在堆上分配,又可能在棧上分配,如果可以在棧上分配線程私有的對象,將減輕內存回收的壓力。另外,C/C++中主要由用戶程序代碼來回收分配的內存,這就不存在無用對象篩選的過程,因此效率上(僅指運行效率,排除了開發效率)也比垃圾收集機制要高。
上面說了一大堆Java語言相對C/C++的劣勢,不是說Java就真的不如C/C++了,相信讀者也注意到了,Java語言的這些性能上的劣勢都是爲了換取開發效率上的優勢而付出的代價,動態安全、動態擴展、垃圾回收這些“拖後腿”的特性都爲Java語言的開發效率做出了很大貢獻。

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