JVM的編譯策略

jvm系列算法

本文主要講述JVM的編譯策略。

解釋器

當虛擬機啓動時,解釋器能夠首先發揮做用,而沒必要等待編譯器所有編譯完成再執行,這樣能夠省去許多沒必要要的編譯時間。而且隨着程序運行時間的推移,編譯器逐漸發揮做用,根據熱點探測功能,,將有價值的字節碼編譯爲本地機器指令,以換取更高的程序執行效率。

hotspot中內嵌有2個JIT編譯器,分別爲Client Compiler,Server Compiler,但大多數狀況下咱們稱之爲C1編譯器和C2編譯器。

C1編譯器

client compiler,又稱C1編譯器,較爲輕量,只作少許性能開銷比較高的優化,它佔用內存較少,適合於桌面交互式應用。在寄存器分配策略上,JDK6之後採用的爲線性掃描寄存器分配算法,其餘方面的優化,主要有方法內聯、去虛擬化、冗餘消除等。

A、方法內聯

多個方法調用,執行時要經歷屢次參數傳遞,返回值傳遞及跳轉等,C1採用方法內聯,把調用到的方法的指令直接植入當前方法中。-XX:+PringInlining來查看方法內聯信息,-XX:MaxInlineSize=35控制編譯後文件大小。

B、去虛擬化

是指在裝載class文件後,進行類層次的分析,若是發現類中的方法只提供一個實現類,那麼對於調用了此方法的代碼,也能夠進行方法內聯,從而提高執行的性能。

C、冗餘消除

在編譯時根據運行時情況進行代碼摺疊或消除。

C2編譯器

Server compiler,稱爲C2編譯器,較爲重量,採用了大量傳統編譯優化的技巧來進行優化,佔用內存相對多一些,適合服務器端的應用。和C1的不一樣主要在於寄存器分配策略及優化範圍,寄存器分配策略上C2採用的爲傳統的圖着色寄存器分配算法,因爲C2會收集程序運行信息,所以其優化範圍更多在於全局優化,不只僅是一個方塊的優化。收集的信息主要有:分支的跳轉/不跳轉的頻率、某條指令上出現過的類型、是否出現過空值、是否出現過異常等。

逃逸分析是C2進行不少優化的基礎,它根據運行狀態來判斷方法中的變量是否會被外部讀取,如不會則認爲此變量是不會逃逸的,那麼在編譯時會作標量替換、棧上分配和同步消除等優化。

(1)標量替換

簡單地說,就是用標量替換聚合量。這樣作的好處是若是建立的對象並未用到其中的所有變量,則能夠節省必定的內存。對於代碼執行而言,無需去找對象的引用,也會更快一些。

(2)棧上分配

若是point沒有逃逸,那麼C2會選擇在棧上直接建立Point對象的實例,而不是在JVM堆上。在棧上分配的好處一方面是加快速度,另外一方面是回收時隨着方法的結束,對象被回收了。

(3)同步消除

若是發現同步的對象未逃逸,那也就沒有必要進行同步了,C2編譯時會直接去掉同步。

C2還會基於擁有的運行信息來作其餘優化,好比編譯分支頻率執行高的代碼等。

運行後C一、C2編譯出來的機器碼若是再也不符合優化條件,則會進行逆優化,也就是回到解釋執行的方式,例如基於類層次分析編譯的代碼,當有新的相應的接口來實現類加入時,就執行逆優化。

OSR編譯

除了C一、C2外,還有OSR(On Stack Replace)編譯,只替換循環代碼體的入口,C一、C2替換的是方法調用的入口。所以OSR編譯後會出現的現象是方法的整段代碼被編譯了,可是隻有循環體部分才執行編譯後的機器碼,其餘部分還是解釋執行。

當機器配置CPU超過2核且內存超過2G,默認爲server模式,32位的windows始終選擇的是client模式。

分層編譯

Java7默認開啓分層編譯(tiered compilation)策略,由C1編譯器和C2編譯器相互協做共同來執行編譯任務。C1編譯器會對字節碼進行簡單和可靠的優化,以達到更快的編譯速度;C2編譯器會啓動一些編譯耗時更長的優化,以獲取更好的編譯質量。

(1)解釋器再也不收集運行狀態信息,只用於啓動並觸發C1編譯
(2)C1編譯後生成帶收集運行信息的代碼
(3)C2編譯,基於C1編譯後代碼收集的運行信息進行激進優化,當激進優化的假設不成立時,再退回使用C1編譯的代碼

程序在未編譯期間解釋執行有個閾值,SunJDK主要依據方法上的兩個計數器是否超過閾值來判斷:

  • A、調用計數器,即方法被調用的次數,CompileThreshold,該值是指當方法被調用多少次後,就編譯爲機器碼,client模式默認爲1500次,server模式默認爲1萬次,能夠在啓動時添加-XX:CompileThreshold=10000來設置該值。

  • B、回邊計數器,即方法中循環執行部分代碼的執行次數,OnStackReplacePercentage,該值用於/參與計算是否觸發OSR編譯的閾值,client默認爲933,sever默認爲140,能夠經過-XX: OnStackReplacePercentage=140來設置。

client模式下的計算規則爲CompileThreshold*OnStackReplacePercentage/100,
server模式下計算規則爲CompileThreshold*(OnStackReplacePercentage-InterpreterProfilePercentage)/100。InterpreterProfilePercentage,默認爲33。

當方法上的回邊計數器到達這個值時,觸發後臺的OSR編譯,並將方法上累積的調用計數器設置爲CompileThreshold 的值,同時將回邊計數器設置爲CompileThreshold/2的值。這樣作一方面是爲了不OSR編譯頻繁被觸發,另外一方面是以便當方法被再次調用時即觸發正常的編譯,當累積的回邊計數器的值再次達到該值時先檢查OSR編譯是否完成,若是已完成,則在執行循環體的代碼時進入編譯後的代碼,若是未完成,繼續把當前回邊計數器的累計值再減掉一些,默認狀況下,對於回邊的狀況,server模式下只要回邊次數達到10700次(10000*(140-33)),就會觸發OSR編譯。

解釋器與編譯器並存

若是選用徹底解釋策略,那麼編譯器將中止全部的工做,字節碼將徹底依靠解釋器逐行解釋執行。
若是選用徹底編譯策略,那麼解釋器仍然會在編譯器沒法進行的特殊狀況下介入運行,這主要是確保程序可以最終順序執行。

SunJDK之因此未選擇在啓動時即編譯成機器碼的緣由以下:
(1)靜態編譯並不能根據程序的運行狀態來優化執行的代碼,C2這種方式是根據運行狀態來進行動態編譯的,例如分支判斷、逃逸分析等,這些措施會對提高程序執行的性能起到很大的幫助,在靜態編譯的狀況下是沒法實現的,給C2收集運行數據越長的時間,編譯出來的代碼會越優。
(2)解釋執行比編譯執行更節省內存
(3)啓動時解釋執行的啓動速度比編譯再啓動更快。

參考

相關文章
相關標籤/搜索