1. 程式人生 > >我看Java虛擬機器(7)---直譯器和JIT編譯器

我看Java虛擬機器(7)---直譯器和JIT編譯器

Java是被定為為解釋性語言,JIT編譯器並不是強制需要的,也並非所有的虛擬機器都是用直譯器+編譯器的並存架構。但主流的商用虛擬機器如Hotspot、J9等都採用這種並存的架構。

直譯器和編譯器比較

直譯器優點:省去編譯時間,啟動速度快
編譯器優點:對程式碼進行優化,執行效率高
兩種方式的優點各為對方的缺點。即直譯器的缺點是執行效率低下,編譯器的缺點是啟動速度慢。很容易理解。

Java虛擬機器

由於Java虛擬機器的平臺無關性,它比C編譯器多了一個位元組碼的步驟,所以會顯得效能效率比較雞肋。所以Java虛擬機器採用這種直譯器和編譯器(兩種模式:client(也叫c1)模式和server(c2)模式)並存的方式,對速度和效率做了一個trade off。
簡單來講,Java虛擬機器就是將使用頻率高的“熱點程式碼”,觸發即時編譯器將其編譯為原生代碼,以後每次使用時直接呼叫原生代碼。那麼問題來了:

  1. 怎麼判定熱點程式碼
  2. 判定出來後怎麼處理

熱點程式碼的判定

目前主要有兩種方式探測熱點程式碼:
1. 基於取樣的熱點探測:虛擬機器週期性檢測各個執行緒的棧頂方法,若是某個方法經常出現,則判定該方法為熱點程式碼。
優點:實現簡單;
缺點:不精確,比如當某個執行緒處於執行緒阻塞時,會擾亂熱點探測。
2. 基於計數器的熱點探測:為每個方法建立計數器,統計執行次數,若超過一定閾(yu)值,則認為它是“熱點方法”。
優點:精確;
缺點:實現複雜,要為每個方法維護計數器。

熱點程式碼的處理

基於計數器的探測,它為每個方法準備了兩個計數器:方法呼叫計數器和回邊計數器。
這兩個計數器都有各自的一個閾值,若超過閾值則觸發JIT編譯。

  1. 方法呼叫計數器:用於統計該方法被呼叫的次數,c1模式下預設5000次,c2模式下10000次。執行過程如下:判斷是否已存在編譯版本,如已存在,則執行編譯版本;否則,方法計數器+1,判斷兩個計數器之和(注意:是方法計數器和回邊計數器的和)是否超過方法計數器閾值,超過則向編譯器提交編譯請求,然後和不超過閾值情況下的處理方式一樣,仍舊解釋執行該方法。
    PS:該計數器並不是絕對次數,而是相對的執行次數,即在一段時間內的執行次數,當超過一定的時間限度,若還是沒有達到閾值,那麼它的計數器會減少一半,此過程被稱為熱度衰減。
  2. 回邊計數器:用於統計方法中迴圈體程式碼的執行次數,位元組碼中遇到控制流向後跳轉的指令稱為“回邊”。建立該計數器的目的就是為觸發OSR(On StackReplacement)編譯,即棧上替換。
    和方法計數器執行過程不同的是:當兩個計數器之和超過閾值的時候,它向編譯器提交OSR編譯,並且調整回邊計數器值
    ,然後仍舊以解釋方式執行下去。
    PS:該計數器是絕對次數,沒有熱度衰減。

其他編譯優化技術

  1. 公共子表示式消除int d = (c * b) * 12 +(a + b * c)不優化時,b*c將會被計算兩次,優化以後就相當於:
E = c * b;
int d = E * 12 + (a + E);

省去一次計算。
2. 陣列邊界檢查消除
比如:前面程式碼使用過f[3],並且判斷了0<=3<=f.length,則下次使用f[3]時,判斷可省略。
3. 方法內聯:對於非虛方法就不講了;對於Java的虛方法,方法內聯就會出現問題,到底選擇哪一個版本的實現,就是一個問題,Java虛擬機器引入了一種名為“型別繼承關係分析”(Class Hierarchy Analysis,CHA)的技術。 該方法檢測出多個版本的實現時,它使用內聯快取,即,第一次呼叫發生時,記錄該接收者資訊,快取該方法,下次發生呼叫時,比較接收者資訊,相同則使用快取中的方法,否則取消該內聯快取。
4.逃逸分析:分析物件的動態作用域,當一個物件在方法裡被定義後,他可能被外部方法呼叫,這種行為被稱為方法逃逸。甚至還可能被外部執行緒訪問,這種行為被稱為執行緒逃逸。 如果能保證一個物件不發生方法逃逸或執行緒逃逸,則就能對這個變數進行一些高校的優化,比如:棧上分配,同步消除,標量替換等。