深入分析JVM執行引擎

2022-08-30 06:02:01

程式和機器溝通的橋樑

一、閒聊

相信很多朋友在出國旅遊,或者與外國友人溝通的過程中,都會遇到語言不通的煩惱。這時候我們就需要掌握對應的外語或者擁有一部翻譯機。而筆者只會中文,所以需要藉助一部翻譯器才能與不懂中文的外國友人交流。咱們的執行引擎就類似於這部「翻譯機」。

二、概述

執行引擎的作用就是將位元組碼指令解釋或者編譯為對應平臺上的本地機器指令。簡單來說,執行引擎充當了將高階語言翻譯為機器語言的翻譯者。對於Hotspot虛擬機器器,執行引擎中包含兩部分:直譯器和JIT編譯器(即時編譯器)。下圖是執行引擎的原理:

三、直譯器

直譯器所承擔的角色就是一個執行時翻譯者,將位元組碼檔案中的內容翻譯為對應平臺的本地機器碼指令。當一條位元組碼指令被解釋執行後,接著再根據pc暫存器中記錄的下一條需要被執行的位元組碼指令執行解釋操作。JVM直譯器一共有兩套,一套是遠古的位元組碼直譯器,另一套是現在普遍使用的模板直譯器

1、位元組碼直譯器

位元組碼直譯器在執行過程中通過純軟體程式碼模擬位元組碼執行,效率非常低。

2、模板直譯器

模板直譯器將每一條位元組碼和一個模板函數關聯,模板函數中直接產生這條位元組碼指令執行時的機器碼,從而提高了直譯器的效能。在常用的HotSpot VM中,直譯器主要由Interpreter模板和code模組構成。Interpreter模板:實現了直譯器的核心功能。code模組:用於管理HotSpot VM在執行時生成的本地機器碼指令。

四、即時編譯器(JIT編譯器)

即時編譯器的目的是避免函數被解釋執行,而是將整個函數體編譯成機器碼指令,每次函數執行時,只執行編譯後的機器碼即可,這種方式可以大大的提高效率。

1、熱點程式碼及探測方式

當然,是否需要JIT編譯器將位元組碼直接編譯成對應平臺的機器碼,需要根據程式碼被呼叫的執行頻率而定。需要被JIT編譯器編譯成機器碼的位元組碼,也稱為熱點程式碼,JIT編譯器會對熱點程式碼做出深度優化,將其從位元組碼編譯成機器碼,並快取到方法區,提高程式碼的執行效率。
JIT編譯的方式發生在方法執行過程中,因此也被稱之為_棧上替換_,或簡稱OSR(On Stack Replacement)編譯。通過熱點探測的方法,判斷一個方法被呼叫多少次,或迴圈體執行多少次才可以達到閾值,進行編譯。而Hotspot VM熱點探測的方式是基於計數器實現的。這種基於技術的熱點探測方式又分為兩種:1.方法呼叫計數器 2.回邊計數器

關於棧上替換這裡筆者不展開贅述,有興趣的小夥伴可以自行了解下

1.1方法呼叫計數器

方法呼叫計數器用於統計方法呼叫次數,它的預設閾值是client模式下是1500次,在server模式下是10000次。超過這個閾值,就會觸發JIT編譯。當然,這個閾值也可以通過修改虛擬機器器引數-XX:CompileThreshold來手動指定。
當一個方法被呼叫的時候,會優先檢查該方法是否被JIT編譯過,如果存在,則優先使用編譯過的原生程式碼來執行,如果不存在,則將此方法的呼叫計數器加一,然後再判斷計數器的值是否超過設定的閾值。如果已經超過了,就會向JIT編譯器提交一個該方法的編譯請求。下面是方法呼叫計數器執行的流程圖:

關於方法呼叫計數器,如果不做任何設定,方法呼叫計數器統計的並不是方法被呼叫的絕對次數,而是一個相對執行的頻率。當超過一定的時間限度,如果方法的呼叫次數仍然達不到閾值,那這個方法的呼叫計數器就會被減少一半,這個過程稱為方法呼叫計數器的熱度衰減,而這段時間被稱作為該方法的半衰週期
進行熱度衰減的過程是虛擬機器器進行垃圾回收的時候順便進行的,舉手之勞而已。可以使用虛擬機器器引數-XX:-UseCounterDecay來關閉熱度衰減。這樣的話,只要執行時間足夠長,絕大部分方法都會被編譯成原生程式碼。最後,還可以使用-XX:CounterHalfLifeTime引數設定半衰週期的時間,單位為秒。

1.2回邊計數器

它的作用是統計一個方法中迴圈體程式碼執行次數,在位元組碼中遇到控制流向後,跳轉的指令稱為「回邊」。顯然,建立回邊計數器統計的目的是為了觸發OSR編譯。下面是回邊計數器執行的流程圖:

關於OSR編譯上文中有提到

2、即時編譯器分類

在Hotspot VM中,內嵌有兩個JIT編譯器,分別為client compiler和server compiler,但是大多數情況下我們簡稱C1編譯器和C2編譯器。可以通過命令顯示的指定JVM在執行時到底使用哪種JIT編譯器。

2.1 c1編譯器

指定Java虛擬機器器執行在client模式下,使用C1編譯器。C1編譯器會對位元組碼進行簡單和可靠的優化,耗時短。以達到更快的編譯速度,但是編譯後的程式碼執行速度相對慢。C1編譯器主要有方法內聯,去虛擬化,冗餘消除。

  1. 方法內聯:將參照的函數程式碼編譯到參照點處,這樣可以減少棧幀的生成,減少引數傳遞以及跳轉過程。
  2. 去虛擬化:對唯一實現的類進行內聯。
  3. 冗餘消除:在執行期間把一些不會執行的程式碼疊掉。

2.2 c2編譯器

指定Java虛擬機器器執行在server模式下,使用C2編譯器。C2編譯器對程式碼優化時間長,編譯時間也長。但是編譯後的程式碼執行速度比較快。C2的優化主要在全域性層面,逃逸分析式優化的基礎。基於逃逸分析,C2上有如下幾種優化:

  1. 標量替換:用標量值代替聚合物件的屬性值。
  2. 棧上分配:對於未逃逸的物件分配在棧上而不是堆上。
  3. 同步消除:清楚同步操作,通常指synchronized。

2.3 Graal編譯器

JDK10起,在C1編譯器和C2編譯器之後,HotSpot VM新增了一個Graal即時編譯器。編譯效果短短几年的時間就追平了C2編譯器。目前,帶著「實驗狀態」標籤,需要使用開關引數-XX:+UnlockExperimentalVMOptions-XX:+UseJVMCICompiler去啟用這個編譯器,才能使用。

五、直譯器和JIT並存

為什麼需要直譯器和JIT並存,原因有幾點:

  1. 當程式啟動的時候,直譯器可以馬上發揮作用,省去編譯的時間。
  2. 編譯器想要執行,需要把位元組碼編譯成本地機器碼,並且快取編譯後的機器碼,編譯需要一定的時間。
  3. 編譯後的本地機器碼,執行效率高。所以,在兩種並存的模式下,直譯器首先發揮作用,而不必等到即時編譯器全部編譯完在執行,這樣可以省去不必要的編譯時間。
  4. 隨著程式繼續不斷執行,編譯器發揮作用,根據熱點探測功能,把越來越多的位元組碼編譯成本地機器碼,獲得更高的執行效率。

六、執行引擎執行程式的方式

在預設的情況下,HotSpot VM採用的是直譯器和JIT編譯器並存的架構,當然讀者可以根據具體的應用場景,通過虛擬機器器引數,為虛擬機器器指定在執行時到底是完全採用直譯器執行,還是完全採用即時編譯器執行。

  1. -Xint:完全採用直譯器模式執行程式
  2. -XComp:完全採用即時編譯器模式執行程式。如果即時編譯器出現問題,直譯器會介入執行;
  3. -Xmixed:採用直譯器+即時編譯器的混合模式共同執行程式,HotStop VM預設就是這個模式。