您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何進行JVM虛擬機中Java的編譯期優化與運行期優化,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
java編譯期優化
java語言的編譯期其實是一段不確定的操作過程,因為它可以分為三類編譯過程:
1.前端編譯:把.java文件轉變為.class文件
2.后端編譯:把字節碼轉變為機器碼
3.靜態提前編譯:直接把.java文件編譯成本地機器代碼
從JDK1.3開始,虛擬機設計團隊就把對性能的優化集中到了后端的即時編譯中,這樣可以讓那些不是由Javac產生的Class文件(如JRuby、Groovy等語言的Class文件)也能享受到編譯期優化所帶來的好處
*Java中即時編譯在運行期的優化過程對于程序運行來說更重要,而前端編譯期在編譯期的優化過程對于程序編碼來說關系更加密切
早期編譯過程主要分為3個部分:1.解析與填充符號表過程:詞法、語法分析;填充符號表 2.插入式注解處理器的注解處理過程 3.語義分析與字節碼生成過程:標注檢查、數據與控制流分析、解語法糖、字節碼生成
Java語言中的泛型只在程序源碼中存在,在編譯后的字節碼文件中,就已經替換成原來的原生類型了,并且在相應的地方插入了強制轉型代碼
泛型擦除前的例子 public static void main( String[] args ) { Map<String,String> map = new HashMap<String, String>(); map.put("hello","你好"); System.out.println(map.get("hello")); } 泛型擦除后的例子 public static void main( String[] args ) { Map map = new HashMap(); map.put("hello","你好"); System.out.println((String)map.get("hello")); }
自動裝箱、拆箱在編譯之后會被轉化成對應的包裝和還原方法,如Integer.valueOf()與Integer.intValue(),而遍歷循環則把代碼還原成了迭代器的實現,變長參數會變成數組類型的參數。
然而包裝類的“==”運算在不遇到算術運算的情況下不會自動拆箱,以及它們的equals()方法不處理數據轉型的關系。
Java語言也可以進行條件編譯,方法就是使用條件為常量的if語句,它在編譯階段就會被“運行”:
public static void main(String[] args) { if(true){ System.out.println("block 1"); } else{ System.out.println("block 2"); } } 編譯后Class文件的反編譯結果: public static void main(String[] args) { System.out.println("block 1"); }
只能是條件為常量的if語句,這也是Java語言的語法糖,根據布爾常量值的真假,編譯器會把分支中不成立的代碼塊消除掉
Java程序最初是通過解釋器進行解釋執行的,當程序需要迅速啟動和執行時,解釋器可以首先發揮作用,省去編譯時間,立即執行;當程序運行后,隨著時間的推移,編譯期逐漸發揮作用,把越來越多的代碼編譯成本地代碼,獲得更高的執行效率。解釋執行節約內存,編譯執行提升效率。 同時,解釋器可以作為編譯器激進優化時的一個“逃生門”,讓編譯器根據概率選擇一些大多數時候都能提升運行速度的優化手段,當激進優化的假設不成立,則通過逆優化退回到解釋狀態繼續執行。
HotSpot虛擬機中內置了兩個即時編譯器,分別稱為Client Compiler(C1編譯器)和Server Compiler(C2編譯器),默認采用解釋器與其中一個編譯器直接配合的方式工作,使用哪個編譯器取決于虛擬機運行的模式,也可以自己去指定。若強制虛擬機運行與“解釋模式”,編譯器完全不介入工作,若強制虛擬機運行于“編譯模式”,則優先采用編譯方式執行程序,解釋器仍然要在編譯無法進行的情況下介入執行過程。
分層編譯策略作為默認編譯策略在JDK1.7的Server模式虛擬機中被開啟,其中包括: 第0層:程序解釋執行,解釋器不開啟性能監控功能,可觸發第1層編譯; 第1層:C1編譯,將字節碼編譯成本地代碼,進行簡單可靠的優化,如有必要將加入性能監控的邏輯; 第2層:C2編譯,也是將字節碼編譯成本地代碼,但是會啟動一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。 實施分層編譯后,C1和C2將會同時工作,C1獲取更高的編譯速度,C2獲取更好的編譯質量,在解釋執行的時候也無須再承擔性能監控信息的任務。
在運行過程中會被即時編譯器編譯的“熱點代碼”有兩類: 1.被多次調用的方法:由方法調用觸發的編譯,屬于JIT編譯方式 2.被多次執行的循環體:也以整個方法作為編譯對象,因為編譯發生在方法執行過程中,因此成為棧上替換(OSR編譯) 熱點探測判定方式有兩種: 1.基于采樣的熱點探測:虛擬機周期性的檢查各個線程的棧頂,如果某個方法經常出現在棧頂,則判定為“熱點方法”。(簡單高效,可以獲取方法的調用關系,但容易受線程阻塞或別的外界因素影響擾亂熱點探測) 2.基于計數的熱點探測:虛擬機為每個方法建立一個計數器,統計方法的執行次數,超過一定閾值就是“熱點方法”。(需要為每個方法維護計數器,不能直接獲取方法的調用關系,但是統計結果精確嚴謹)
HotSpot虛擬機使用的是第二種,它為每個方法準備了兩類計數器:方法調用計數器和回邊計數器,下圖表示方法調用計數器觸發即時編譯:
如果不做任何設置,執行引擎會繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成,下次調用才會使用已編譯的版本。另外,方法調用計數器的值也不是一個絕對次數,而是一段時間之內被調用的次數,超過這個時間,次數就減半,這稱為計數器熱度的衰減。
下圖表示回邊計數器觸發即時編譯:
回邊計數器沒有計數器熱度衰減的過程,因此統計的就是絕對次數,并且當計數器溢出時,它還會把方法計數器的值也調整到溢出狀態,這樣下次進入該方法的時候就會執行標準編譯過程。
虛擬機設計團隊幾乎把對代碼的所有優化措施都集中在了即時編譯器之中,那么在編譯器編譯的過程中,到底做了些什么事情呢?下面將介紹幾種最有代表性的優化技術:
公共子表達式消除
如果一個表達式E已經計算過了,并且先前的計算到現在E中所有變量的值都沒有發生變化,那么E的這次出現就成為了公共表達式,可以直接用之前的結果替換。
例:int d = (c
b) 12 + a + (a + b
c) => int d = E 12 + a + (a + E)
數組邊界檢查消除
Java語言中訪問數組元素都要進行上下界的范圍檢查,每次讀寫都有一次條件判定操作,這無疑是一種負擔。編譯器只要通過數據流分析就可以判定循環變量的取值范圍永遠在數組長度以內,那么整個循環中就可以把上下界檢查消除,這樣可以省很多次的條件判斷操作。
另一種方法叫做隱式異常處理,Java中空指針的判斷和算術運算中除數為0的檢查都采用了這個思路:
if(foo != null){ return foo.value; }else{ throw new NullPointException(); } 使用隱式異常優化以后: try{ return foo.value; }catch(segment_fault){ uncommon_trap(); } 當foo極少為空時,隱式異常優化是值得的,但是foo經常為空,這樣的優化反而會讓程序變慢,而HotSpot虛擬機會根據運行期收集到的Profile信息自動選擇最優方案。
方法內聯
方法內聯能去除方法調用的成本,同時也為其他優化建立了良好的基礎,因此各種編譯器一般會把內聯優化放在優化序列的最靠前位置,然而由于Java對象的方法默認都是虛方法,因此方法調用都需要在運行時進行多態選擇,為了解決虛方法的內聯問題,首先引入了“類型繼承關系分析(CHA)”的技術。
1.在內聯時,若是非虛方法,則可以直接內聯 2.遇到虛方法,首先根據CHA判斷此方法是否有多個目標版本,若只有一個,可以直接內聯,但是需要預留一個“逃生門”,稱為守護內聯,若在程序的后續執行過程中,加載了導致繼承關系發生變化的新類,就需要拋棄已經編譯的代碼,退回到解釋狀態執行,或者重新編譯。 3.若CHA判斷此方法有多個目標版本,則編譯器會使用“內聯緩存”,第一次調用緩存記錄下方法接收者的版本信息,并且每次調用都比較版本,若一致則可以一直使用,若不一致則取消內聯,查找虛方法表進行方法分派。
逃逸分析
逃逸分析的基本行為就是分析對象動態作用域,當一個對象被外部方法所引用,稱為方法逃逸;當被外部線程訪問,稱為線程逃逸。若能證明一個對象不會被外部方法或進程引用,則可以為這個變量進行一些優化:
1.棧上分配:如果確定一個對象不會逃逸,則可以讓它分配在棧上,對象所占用的內存空間就可以隨棧幀出棧而銷毀。這樣可以減小垃圾收集系統的壓力。 2.同步消除:線程同步相對耗時,如果確定一個變量不會逃逸出線程,那這個變量的讀寫不會有競爭,則對這個變量實施的同步措施也就可以消除掉。 3.標量替換:如果逃逸分析證明一個對象不會被外部訪問,并且這個對象可以被拆散的話,那么程序真正執行的時候可以不創建這個對象,改為直接創建它的成員變量,這樣就可以在棧上分配。
可是目前還不能保證逃逸分析的性能收益必定高于它的消耗,所以這項技術還不是很成熟。
Java虛擬機的即時編譯器與C/C++的靜態編譯器相比,可能會由于下面的原因導致輸出的本地代碼有一些劣勢: 1.即時編譯器運行占用的是用戶程序的運行時間,具有很大的時間壓力,因此不敢隨便引入大規模的優化技術; 2.Java語言是動態的類型安全語言,虛擬器需要頻繁的進行動態檢查,如空指針,上下界范圍,繼承關系等; 3.Java中使用虛方法頻率遠高于C++,則需要進行多態選擇的頻率遠高于C++; 4.Java是可以動態擴展的語言,運行時加載新的類可能改變原有的繼承關系,許多全局的優化措施只能以激進優化的方式來完成; 5.Java語言的對象內存都在堆上分配,垃圾回收的壓力比C++大 然而,Java語言這些性能上的劣勢換取了開發效率上的優勢,并且由于C++編譯器所有優化都是在編譯期完成的,以運行期性能監控為基礎的優化措施都無法進行,這也是Java編譯器獨有的優勢。
關于如何進行JVM虛擬機中Java的編譯期優化與運行期優化就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。