JVM執行引擎


1.執行引擎是Java虛擬機的核心組成部分之一

2.虛擬機是一個相對於“物理機”的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、緩存、指令集和操作系統層面上的。

而虛擬機的執行引擎則是由軟件自行實現的,因此可以不受物理條件制約地定制指令集與執行引擎的結構體系,能夠執行那些不被硬件直接支持的指令集格式。

3.JVM的主要任務是負責裝載字節碼到其內部,但字節碼並不能夠直接運行在操作系統之上,因為字節碼指令並非等價於本地機器指令,它內部包含的僅僅只是一些能夠被JVM鎖識別的字節碼指令、符號表和其他輔助信息

4.那么,如果想讓一個Java程序運行起來、執行引擎的任務就是將字節碼指令解釋/編譯為對應平台上的本地機器指令才可以。簡單來說,JVM中的執行引擎充當了將高級語言翻譯為機器語言的譯者.

上圖:

執行引擎的工作過程

  • 從外觀上來看,所有的Java虛擬機的執行引擎輸入、輸出都是一致的:輸入的是字節碼二進制流,處理過程是字節碼解析執行的等效過程,輸出的是執行結果。
  • 執行引擎在執行的過程中究竟需要執行什么樣的字節碼指令完全依賴於PC寄存器。每當執行完一項指令操作后,PC寄存器就會更新下一條需要被執行的指令地址。
  • 當然方法在執行的過程中,執行引擎有可能會通過存儲在局部變量表(棧)中的對象引用准確定位到存儲在Java堆區中的對象實例信息。
  • 以及通過對象頭(堆)中的元數據指針,定位到目標對象的類型信息(方法區)。(下圖堆區與方法區之間,其實還存在類型指針:從堆區對象實例指向方法區的類元數據InstanceKlass)

 上圖:

  

Java代碼編譯和執行過程

 Java代碼編譯是由Java源碼編譯器(Java前端編譯器)來完成(與JVM無關),流程圖如下所示:

 Java字節碼的執行是由JVM執行引擎來完成,流程圖如下所示:

為什么說Java是半解釋半編譯型語言?

JDK1.0時代,將Java語言定位為“解釋執行”還是比較准確的(先編譯成字節碼,再對字節碼逐行解釋執行interpreter)。

再后來,Java也發展出可以直接生成本地代碼的編譯器(這里指的是后端編譯器JIT,將源代碼直接編譯成和本地機器平台相關的機器語言)。

基於目前JVM執行引擎當中解釋器與即時編譯器共存

現在JVM在執行Java代碼的時候,會將解釋執行與編譯執行二者結合起來進行。如今Java采用的是解釋和編譯混合的模式:

執行引擎獲取到,由javac將源碼編譯成字節碼文件class.之后,

然后在運行的時候通過解釋器interpreter轉換成最終的機器碼。(解釋型)

另外JVM平台支持一種叫作即時編譯的技術。即時編譯的目的是避免函數被解釋執行,而是將整個函數體編譯成為機器碼,這種方式可以使執行效率大幅度提升(直接編譯型)

解釋器(interpreter)

當Java虛擬機啟動時會根據預定義的規范對字節碼采用逐行解釋的方式執行,將每條字節碼文件中的內容“翻譯”為對應平台的本地機器指令執行。

  • 解釋器真正意義上所承擔的角色就是一個運行時“翻譯者”,將字節碼文件中的內容“翻譯”為對應平台的本地機器指令執行。
  • 當一條字節碼指令被解釋執行完成后,接着再根據PC寄存器中記錄的下一條需要被執行的字節碼指令執行解釋操作。

在HotSpot VM中,解釋器主要由Interpreter模塊和Code模塊構成。

  • Interpreter模塊:實現了解釋器的核心功能
  • Code模塊:用於管理HotSpot VM在運行時生成的本地機器指令

JIT (Just In Time Compiler):即時編譯器

  • 由於解釋器在設計和實現上非常簡單,因此除了Java語言之外,還有許多高級語言同樣也是基於解釋器執行的,比如Python、 Perl、Ruby等。但是在今天,基於解釋器執行已經淪落為低效的代名詞,並且時常被一些C/C+ +程序員所調侃。
  • 為了解決這個問題,JVM平台支持一種叫作即時編譯的技術。即時編譯的目的是避免函數被解釋執行,而是將整個函數體編譯成為機器碼,每次函數執行時,只執行編譯后的機器碼即可,這種方式可以使執行效率大幅度提升
  • 不過無論如何,基於解釋器的執行模式仍然為中間語言的發展做出了不可磨滅的貢獻。

熱點代碼及探測方式

      當然是否需要啟動JIT編譯器將字節碼直接編譯為對應平台的本地機器指令,則需要根據代碼被調用執行的頻率而定。關於那些需要被編譯為本地代碼的字節碼,也被稱之為“熱點代碼”。JIT編譯器在運行時會針對那些頻繁被調用的“熱點代碼”做出深度優化,將其直接編譯為對應平台的本地機器指令,以此提升Java程序的執行性能。

  • 一個被多次調用的方法,或者是一個方法體內部循環次數較多的循環體都可以被稱之為“熱點代碼”,因此都可以通過JIT編譯器編譯為本地機器指令。由於這種編譯方式發生在方法的執行過程中,因此也被稱之為棧上替換,或簡稱為OSR (On StackReplacement)編譯。
  • 一個方法究竟要被調用多少次,或者一個循環體究竟需要執行多少次循環才可以達到這個標准?必然需要一個明確的閾值,JIT編譯器才會將這些“熱點代碼”編譯為本地機器指令執行。這里主要依靠 熱點探測功能。
  • 目前HotSpot VM所采用的熱點探測方式是基於計數器的熱點探測。
  • 采用基於計數器的熱點探測,HotSpot VM將會為每一個 方法都建立2個不同類型的計數器,分別為方法調用計數器(Invocation Counter) 和回邊計數器(BackEdge Counter) 。
    • 方法調用計數器用於統計方法的調用次數。
    • 回邊計數器則用於統計循環體執行的循環次數。

方法調用計數器

  • 這個計數器就用於統計方法被調用的次數,它的默認閾值在Client 模式 下是1500 次,在Server 模式下是10000 次。超過這個閾值,就會觸發JIT編譯。
  • 這個閾值可以通過虛擬機參數-XX :CompileThreshold來人為設定。
  • 當一個方法被調用時, 會先檢查該方法是否存在被JIT編譯過的版本,如 果存在,則優先使用編譯后的本地代碼來執行。如果不存在已被編譯過的版本,則將此方法的調用計數器值加1,然后判斷方法調用計數器與回邊計數器值之和是否超過方法調用計數器的閾值。如果已超過閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求。

 注意:熱頻代碼片段經過即時編譯后的產物--機器指令,需要緩存起來Code Cache,存放在方法區(元空間/本地內存)

熱度衰減(現實當中的殘酷概念,好理解,但不好受)

  • 如果不做任何設置,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被調用的次數。
  • 當超過一定的時間限度, 如果方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減少一半,這個過程稱為方法調用計數器熱度的衰減(Counter Decay) ,而這段時間就稱為此方法統計的半衰周期(Counter Half Life Time)。
  • 進行熱度衰減的動作是在虛擬機進行垃圾收集時順便進行的,可以使用虛擬機參數 -XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法調用的絕對次數,這樣,只要系統運行時間足夠長,絕大部分方法都會被編譯成本地代碼。
  • 另外, 可以使用-XX:CounterHalfLifeTime參數設置半衰周期的時間,單位是秒。

回邊計數器

它的作用是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向后跳轉的指令稱為“回邊” (Back Edge)。顯然,建立回邊計數器統計的目的就是為了觸發OSR編譯。

 

 

解釋器依然存在的必要性

有些開發人員會感覺到詫異,既然HotSpotVM中已經內置JIT編譯器了,那么為什么還需要再使用解釋器來“拖累”程序的執行性能呢?比如JRockit VM內部就不包含解釋器,字節碼全部都依靠即時編譯器編譯后執行。

首先明確
當程序啟動后,解釋器可以馬上發揮作用,省去編譯的時間,立即執行。 編譯器要想發揮作用,把代碼編譯成本地代碼,需要一定的執行時間。但編譯為本地代碼后,執行效率高。

所以
盡管JRockitVM中程序的執行性能會非常高效,但程序在啟動時必然需要花費更長的時間來進行編譯。對於服務端應用來說,啟動時間並非是關注重點,但對於那些看中啟動時間的應用場景而言,或許就需要采用解釋器與即時編譯器並存的架構來換取一一個平衡點。在此模式下,當Java虛擬器啟動時,解釋器可以首先發揮作用,而不必等待即時編譯器全部編譯完成后再執行,這樣可以省去許多不必要的編譯時間。隨着時間的推移,編譯器發揮作用,把越來越多的代碼編譯成本地代碼,獲得更高的執行效率。

同時,解釋執行在編譯器進行激進優化不成立的時候,作為編譯器的“逃生門”。

HostSpot JVM的執行方式

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

HotSpot VM 可以設置程序執行方式

缺省情況下HotSpot VM是采用解釋器與即時編譯器並存的架構,當然開發人員可以根據具體的應用場景,通過命令顯式地為Java虛擬機指定在運行時到底是完全采用解釋器執行,還是完全采用即時編譯器執行。如下所示:

  • -Xint: 完全采用解釋器模式執行程序;
  • -Xcomp: 完全采用即時編譯器模式執行程序。如果即時編譯出現問題,解釋器會介入執行。
  • -Xmixed:采用解釋器+即時編譯器的混合模式共同執行程序。

測試表明:

  • 純解釋器模式速度最慢(JVM1.0版本用的就是純解釋器執行)
  • 混合模式速度更快
/**
 * 測試解釋器模式和JIT編譯模式
 *  -Xint  : 6520ms
 *  -Xcomp : 950ms
 *  -Xmixed : 936ms
 */
public class IntCompTest {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        testPrimeNumber(1000000);
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為:" + (end - start));
    }

    public static void testPrimeNumber(int count){
        for (int i = 0; i < count; i++) {
            //計算100以內的質數
            label:for(int j = 2;j <= 100;j++){
                for(int k = 2;k <= Math.sqrt(j);k++){
                    if(j % k == 0){
                        continue label;
                    }
                }
                //System.out.println(j);
            }

        }
    }
}

 

宕機案例,熱機狀態切換

  注意解釋執行與編譯執行在線上環境微妙的辯證關系。機器在熱機狀態可以承受的負載要大於冷機狀態。如果以熱機狀態時的流量進行切流,可能使處於冷機狀態的服務器因無法承載流量而假死。
  在生產環境發布過程中,以分批的方式進行發布,根據機器數量划分成多個批次,每個批次的機器數至多占到整個集群的1/8。曾經有這樣的故障案例:某程序員在發布平台進行分批發布,在輸入發布總批數時,誤填寫成分為兩批發布。如果是熱機狀態,在正常情況下一半的機器可以勉強承載流量,但由於剛啟動的JVM均是解釋執行,還沒有進行熱點代碼統計和JIT動態編譯,導致機器啟動之后,當前1/2發布成功的服務器馬上全部宕機,此故障說明了JIT的存在。一阿里團隊

機器碼、指令、匯編語言

機器碼

  • 各種用二進制編碼方式表示的指令(010101........),叫做機器指令碼。開始,人們就用它采編寫程序,這就是機器語言。
  • 機器語言雖然能夠被計算機理解和接受,但和人們的語言差別太大,不易被人們理解和記憶,並且用它編程容易出差錯。
  • 用它編寫的程序一經輸入計算機,CPU直接讀取運行,因此和其他語言編的程序相比,執行速度最快。
  • 機器指令與CPU緊密相關,所以不同種類的CPU所對應的機器指令也就不同。

指令

  • 由於機器碼是有0和1組成的二進制序列,可讀性實在太差,於是人們發明了指令。
  • 指令就是把機器碼中特定的0和1序列,簡化成對應的指令(一般為英文簡寫,如mov,inc等),可讀性稍好
  • 由於不同的硬件平台,執行同一個操作,對應的機器碼可能不同,所以不同的硬件平台的同一種指令(比如mov),對應的機器碼也可能不同。

指令集

  • 不同的硬件平台,各自支持的指令,是有差別的。因此每個平台所支持的指令,稱之為對應平台的指令集。
  • 如常見的
    • x86指令集,對應的是x86架構的平台
    • ARM指令集,對應的是ARM架構的平台

匯編語言

  • 由於指令的可讀性還是太差,於是人們又發明了匯編語言。
  • 在匯編語言中,用助記符(Mnemonics)代替機器指令的操作碼,用地址符號(Symbol)或標號(Label)代替指令或操作數的地址。
  • 在不同的硬件平台,匯編語言對應着不同的機器語言指令集,通過匯編過程轉換成機器指令。
    • 由於計算機只認識指令碼,所以用匯編語言編寫的程序還必須翻譯成機器指令碼,計算機才能識別和執行。

高級語言

  • 為了使計算機用戶編程序更容易些,后來就出現了各種高級計算機語言。高級語言比機器語言、匯編語言更接近人的語言
  • 當計算機執行高級語言編寫的程序時,仍然需要把程序解釋和編譯成機器的指令碼。完成這個過程的程序就叫做解釋程序或編譯程序。

最后補充幾種編譯器

  • 前端編譯器:把.java文件轉變成.class文件。包括Sun的Javac、Eclipse JDT中的增量式編輯器(ECJ)
  • 后端運行期即時編譯器(JIT編譯器,Just In Time Compiler):把字節碼轉成機器碼。包括HotSpot VM的C1、C2編譯器
  • 靜態提前編譯器(AOT編譯器,Ahead Of Time Compiler):把*.java編譯成本地機器碼。包括GNU Compiler for the Java(GCJ)、Excelsior JET

本篇內容重點強調第二種,也就是我們的JIT。

HotSpot VM 中的JIT分類

在HotSpot VM中內嵌有兩個JIT編譯器,分別為Client Compiler和Server Compiler,但大多數情況下我們簡稱為C1編譯器和C2編譯器。開發人員可以通過如下命.令顯式指定Java虛擬機在運行時到底使用哪一種即時編譯器,如下所示:

  • -client: 指定Java虛擬機運行在Client模式下,並使用C1編譯器;
    • C1編譯器會對字節碼進行簡單和可靠的優化,耗時短。以達到更快的編譯速度。
  • -server: 指定Java虛擬機運行在Server模式下,並使用C2編譯器。
    • C2進行耗時較長的優化,以及激進優化。但優化的代碼執行效率更高。
注意:64位操作系統默認使用-server服務器模式,即C2編譯器。

C1和C2編譯器不同的優化策略

  • 在不同的編譯器上有不同的優化策略,C1編譯器上主要有方法內聯,去虛擬化、冗余消除。
    • 方法內聯:將引用的函數代碼編譯到引用點處,這樣可以減少棧幀的生成,減少參數傳遞以及跳轉過程
    • 去虛擬化:對唯一的實現類進行內聯
    • 冗余消除:在運行期間把一些不會執行的代碼折疊掉
  • C2的優化主要是在全局層面,逃逸分析是優化的基礎。基於逃逸分析在C2.上有如下幾種優化:(server模式下才會有這些優化,64位系統默認就是server模式)
    • 標量替換:用標量值代替聚合對象的屬性值
    • 棧上分配:對於未逃逸的對象分配對象在棧而不是堆
    • 同步消除:清除同步操作,通常指synchronized
  • C2編譯器啟動時長比C1編譯器慢,系統穩定執行以后,C2編譯器執行速度遠遠快於C1編譯器。
  • 程序解釋執行(不開啟性能監控)可以觸發C1編譯,將字節碼編譯成機器碼,可以進行簡單優化,也可以加上性能監控,C2編譯會根據性能監控信息進行激進優化
  • 不過在Java7版本之后,一旦開發人員在程序中顯式指定命令“-server"時,默認將會開啟分層編譯策略,由C1編譯器和C2編譯器相互協作共同來執行編譯任務。

Graal編譯器

  • 自JDK10起,HotSpot又加入一個全新的即時編譯器: Graal編譯器
  • 編譯效果短短幾年時間就追評了C2編譯器。未來可期。
  • 目前,帶着“實驗狀態"標簽,需要使用開關參數 -XX: +UnlockExperimentalVMOptions 一XX: +UseJVMCICompiler去激活,才可以使用。

AOT編譯器

  • jdk9引入了AOT編譯器(靜態提前編譯器,Ahead Of Time Compiler)
  • Java 9引入了實驗性AOT編譯工具jaotc。它借助了Graal 編譯器,將所輸入的Java 類文件轉換為機器碼,並存放至生成的動態共享庫之中。
  • 所謂AOT編譯,是與即時編譯相對立的一個概念。我們知道,即時編譯指的是在程序的運行過程中,將字節碼轉換為可在硬件上直接運行的機器碼,並部署至托管環境中的過程。而AOT編譯指的則是,在程序運行之前,便將字節碼轉換為機器碼的過程。
  • 最大好處: Java虛擬機加載已經預編譯成二進制庫,可以直接執行。不必等待即時編譯器的預熱,減少Java應用給人帶來“第一次運行慢”的不良體驗。
  • 缺點:
    • 破壞了java"一次編譯,到處運行”(提前干掉了能夠跨平台的class文件),必須為每個不同硬件、oS編譯對應的發行包。
    • 降低了Java鏈接過程的動態性,加載的代碼在編譯期就必須全部已知。
    • 還需要繼續優化中,最初只支持Linux x64 java base


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM