Java即時編譯器JIT之簡單介紹
本文源於深入Java虛擬機器一書,提取部分骨幹內容,算是讀書筆記吧。
問題:
為何JVM需要使用直譯器和編譯器並存的架構?
JVM為什麼要實現兩個不同的即時編譯器?
程式何時會使用直譯器執行?何時會使用編譯器執行?
哪些程式程式碼會被編譯成為原生代碼?如何編譯?
Java程式碼的執行效率就一定比C,C++靜態執行的執行差?Java程式碼解析執行有何優勢?
序:
從計算機程式出現的第一天起,對效率的追逐就是程式天生的堅定的信仰,這個過程猶如一場沒有終點,永不停歇的F1方程式競賽。
程式設計師猶如車手,技術平臺則是賽道上飛馳的賽車。
JVM即時編譯器
即時編譯器(Just In Time Compiler) 簡稱JIT
Java程式最初是通過直譯器(Interpreter)進行解釋執行的,當JVM發現某個方法或程式碼塊執行特別頻繁的時候,就會認為這是“熱點程式碼”(Hot Spot Code)。
為了提高熱點程式碼的執行效率,就會將這些“熱點程式碼”編譯成與本地機器相關的機器碼,進行各個層次的優化。 完成這個任務的編譯器就是即時編譯器(JIT)。
即時編譯器的效能好壞,程式碼的優化程度高低是衡量一款商用虛擬機器優秀與否的最關鍵指標之一,它是虛擬機器最核心最能體現技術水平的部分。
JVM的兩款即時編譯器JIT
JVM中預設內建了兩款即時編譯器,稱為Client Compiler和Server Compiler。
可以用指定引數的方式,指定採用Client模式和Server模式。預設是mixed模式。
java –Xint 解析 java –Xcomp 編譯
解析器與編譯器並存:
1、當程式需要迅速啟動和執行的時候,解析器首先發揮作用,省去編譯的時間,立即執行。隨著時間的推移,編譯器發揮作用,把越來越多的程式碼編譯成原生代碼,獲得更高的執行效率。
2、當機器記憶體限制比較大,可以用解析方式節約記憶體,反之可以用編譯提升效率。
3、解析器還可以作為編譯器的“逃生門”。當例如載入了新類後型別結構發生變化,可以採用逆優化,退回到解析狀態繼續執行。
Client Compiler和Server Compiler
Client Compiler和Server Compiler會實現分層編譯(JDK 1.7預設有)。
第0層 程式解析執行,解析器不開啟效能監控,可觸發第一層編譯。
第1層 編譯成本地相關程式碼,進行簡單優化
第2層 除編譯成本地相關程式碼外,還進行成編譯耗時較長的優化。
Client Compiler獲得更高的編譯速度 Server Compiler獲得更好的編譯質量,無須承擔效能監控的任務。
熱點程式碼
兩類:1、多次被呼叫的方法
2、多次被執行的迴圈體
熱點判定方式,兩種:
1、基於取樣的方式探測(Sample Based Hot Spot Detection) 。週期性檢測各個執行緒的棧頂,發現某個方法經常出險在棧頂,就認為是熱點方法。好處就是簡單,缺點就是無法精確確認一個方法的熱度。容易受執行緒阻塞或別的原因干擾熱點探測。
2、基於計數器的熱點探測(Counter Based Hot Spot Detection)。某個方法超過閥值就認為是熱點方法,觸發JIT編譯。(涉及計數器的熱度半衰減過程)
兩個計數器:方法呼叫計數器(Invocation Counter)和回邊計數器(Back Edge Counter)
觸發了JIT編譯,編譯工作完成,這個方法的入口就會被系統自動改為新的編譯入口,就會呼叫編譯的版本。
JVM編譯優化技術-----逃逸分析
JDK設計團隊,幾乎把所有的優化措施都集中在即時編譯器中。一般認為,編譯器的本地會比javac的產生的位元組碼更優秀。
常見的優化技術很多:例如公共表示式的消除,陣列邊界檢查的消除,方法的內聯(最重要的優化技術)
介紹一個JVM最前沿的優化技術:逃逸分析:
逃逸分析:當一個物件被定義後,可能被外部方法引用,例如被當作引數傳遞到其他方法中,稱為方法逃逸。可以被其他執行緒訪問,這個稱為執行緒逃逸。
若能證明這個物件不會逃逸到其他方法或執行緒中,就可以進行高效的優化。
1、棧上分配 在堆上分配物件記憶體,回收整理記憶體需要消耗時間,若在棧上分配記憶體將是個不錯的主意。被物件佔用的空間就可以隨幀棧的就出棧而銷燬。大量的物件隨方法的結束而自動銷燬,GC也減輕壓力。
2、同步消除 若不會執行緒逃逸,不會有競爭,方法上的同步措施就會消除。
3、標量替換 Java的原始型別無法再分就是一個標量,物件就是聚合量。物件若可以被拆分成標量,直接在棧上分配,就是類似棧上分配記憶體,甚至分配到快取記憶體中。
逃逸分析不成熟的原因:不能保證逃逸分析的效能收益大於它的消耗。
Java與C/C++編譯器對比
Java與C/C++編譯器的對比,代表了最經典的即時編譯器與靜態編譯器的對比。除了自身API的實現的好壞,更多是一場“拼編譯器”和“拼輸出程式碼質量”的遊戲。
看看這兩種語言的編譯器的優劣:
1、即時編譯器執行佔用的是使用者的執行時間,具有很大的時間壓力,它提供的優化手段嚴重受制與編譯的成本。而編譯的時間在靜態編譯中並不是主要關注點。
2、Java語言是動態的型別安全語言,意味著由虛擬機器確保程式不會違反語言的語義或訪問非結構化記憶體。虛擬機器需要頻繁的動態檢查,如空指標,陣列越界,繼承關係等,總體消耗不少時間。
3、Java語言使用虛方法的頻率遠大於C/C++,意味對方法的接收者進行多型的選擇頻率遠大於C/C++,意味著即時編譯器的優化難度遠遠大於C/C++編譯器。
4、Java語言是動態擴充套件的語言,執行時會載入新的類,改變程式的繼承關係,使得很多全域性優化難以進行。只能採用激進的方式,在執行時撤銷或重新進行一些優化。
5、Java語言的物件是在堆上分配,只有方法的區域性變數才在棧上分配。而C/C++語言有多種分配方式,既可以在堆上分配,又可以在棧上分配,減輕了記憶體回收的壓力。另外C/C++語言主要由使用者程式碼回收記憶體,不存在無用物件篩選的過程,效率要比垃圾回收機制要高。
但是:
Java語言在效能上的劣勢都是為了換取開發效率上的優勢而付出的代價。動態安全,動態擴充套件,垃圾回收這些“拖後腿”的特性都是為Java的開發效率做出了很大的貢獻。
何況Java即時編譯器能做的,C/C++的靜態優化編譯器不一定能夠做:
由於C/C++的靜態編譯,以執行效能監控為基礎的優化措施它都無法進行,如呼叫頻率預測,分支頻率預測,裁剪未使用分支等,這些都是稱為Java語言獨有的效能優勢。