JVM執行引擎

轉自:https://mp.weixin.qq.com/s/rXdd7zEJxY4SBSSAg5Dw3w

我們都知道,在當前的Java中(1.0)之後,編譯器講源代碼轉成字節碼,那麼字節碼如何被執行的呢?這就涉及到了JVM的字節碼執行引擎,執行引擎負責具體的代碼調用及執行過程。就目前而言,所有的執行引擎的基本一致:

  1. 輸入:字節碼文件

  2. 處理:字節碼解析

  3. 輸出:執行結果。

物理機的執行引擎是由硬件實現的,和物理機的執行過程不同的是虛擬機的執行引擎由於自己實現的。


運行時候的棧結構

每一個線程都有一個棧,也就是前文中提到的虛擬機棧,棧中的基本元素我們稱之爲棧幀。棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構。每個棧幀都包括了一下幾部分:局部變量表、操作數棧、動態連接、方法的返回地址 和一些額外的附加信息。棧幀中需要多大的局部變量表和多深的操作數棧在編譯代碼的過程中已經完全確定,並寫入到方法表的Code屬性中。在活動的線程中,位於當前棧頂的棧幀纔是有效的,稱之爲當前幀,與這個棧幀相關聯的方法稱爲當前方法。執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。需要注意的是一個棧中能容納的棧幀是受限,過深的方法調用可能會導致StackOverFlowError,當然,我們可以認爲設置棧的大小。其模型示意圖大體如下: 


針對上面的棧結構,我們重點解釋一下局部變量表,操作棧,指令計數器幾個概念:

1、局部變量表

是變量值的存儲空間,由方法參數和方法內部定義的局部變量組成,其容量用Slot1作爲最小單位。在編譯期間,就在方法的Code屬性的max_locals數據項中確定了該方法所需要分配的局部變量表的最大容量。由於局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。如果是實例方法,那局部變量表第0位索引的Slot存儲的是方法所屬對象實例的引用,因此在方法內可以通過關鍵字this來訪問到這個隱含的參數。其餘的參數按照參數表順序排列,參數表分配完畢之後,再根據方法體內定義的變量的順序和作用域分配。我們知道類變量表有兩次初始化的機會,第一次是在“準備階段”,執行系統初始化,對類變量設置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。和類變量初始化不同的是,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量則必須人爲的初始化,否則無法使用。舉例說明:

爲了方便起見,假設以上兩段代碼在同一個類中。這時call()所對應的棧幀中的局部變量表大體如下: 
 
而call2()所對應的棧幀的局部變量表大體如下: 


2、操作數棧

後入先出棧,由字節碼指令往棧中存數據和取數據,棧中的任何一個元素都是可以任意的Java數據類型。和局部變量類似,操作數棧的最大深度也在編譯的時候寫入到Code屬性的max_stacks數據項中。當一個方法剛開始執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令往操作數中寫入和提取內容,也就是出棧/入棧操作。操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配2,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。另外我們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操作數棧。


3、動態連接

每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有該引用是爲了支持方法調用過程中的動態連接。


4、方法返回地址

存放調用調用該方法的pc計數器的值。當一個方法開始之後,只有兩種方式可以退出這個方法:1、執行引擎遇到任意一個方法返回的字節碼指令,也就是所謂的正常完成出口。2、在方法執行的過程中遇到了異常,並且這個異常沒有在方法內進行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種方式成爲異常完成出口。正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值。 
無論通過哪種方式退出,在方法退出後都返回到該方法被調用的位置,方法正常退出時,調用者的pc計數器的值作爲返回地址,而通過異常退出的,返回地址是要通過異常處理器表來確定,棧幀中一般不會保存這部分信息。本質上,方法的退出就是當前棧幀出棧的過程。


方法調用

方法調用的主要任務就是確定被調用方法的版本(即調用哪一個方法),該過程不涉及方法具體的運行過程。按照調用方式共分爲兩類:

  1. 解析調用是靜態的過程,在編譯期間就完全確定目標方法。

  2. 分派調用即可能是靜態,也可能是動態的,根據分派標準可以分爲單分派和多分派。兩兩組合有形成了靜態單分派、靜態多分派、動態單分派、動態多分派

解析

在Class文件中,所有方法調用中的目標方法都是常量池中的符號引用,在類加載的解析階段,會將一部分符號引用轉爲直接引用,也就是在編譯階段就能夠確定唯一的目標方法,這類方法的調用成爲解析調用。此類方法主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可訪問,因此決定了他們都不可能通過繼承或者別的方式重寫該方法,符合這兩類的方法主要有以下幾種:靜態方法、私有方法、實例構造器、父類方法。虛擬機中提供了以下幾條方法調用指令:

  1. invokestatic:調用靜態方法,解析階段確定唯一方法版本

  2. invokespecial:調用<init>方法、私有及父類方法,解析階段確定唯一方法版本

  3. invokevirtual:調用所有虛方法

  4. invokeinterface:調用接口方法

  5. invokedynamic:動態解析出需要調用的方法,然後執行

前四條指令固化在虛擬機內部,方法的調用執行不可認爲干預,而invokedynamic指令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法,其餘的(final修飾的除外[^footnote4])稱爲虛方法。

分派

分派調用更多的體現在多態上。

  1. 靜態分派:所有依賴靜態類型3來定位方法執行版本的分派成爲靜態分派,發生在編譯階段,典型應用是方法重載。

  2. 動態分派:在運行期間根據實際類型4來確定方法執行版本的分派成爲動態分派,發生在程序運行期間,典型的應用是方法的重寫。

  3. 單分派:根據一個宗量5 對目標方法進行選擇。

  4. 多分派:根據多於一個宗量對目標方法進行選擇。

JVM實現動態分派

動態分派在Java中被大量使用,使用頻率及其高,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率,因此JVM在類的方法區中建立虛方法表(virtual method table)來提高性能。每個類中都有一個虛方法表,表中存放着各個方法的實際入口。如果某個方法在子類中沒有被重寫,那子類的虛方法表中該方法的地址入口和父類該方法的地址入口一樣,即子類的方法入口指向父類的方法入口。如果子類重寫父類的方法,那麼子類的虛方法表中該方法的實際入口將會被替換爲指向子類實現版本的入口地址。 
那麼虛方法表什麼時候被創建?虛方法表會在類加載的連接階段被創建並開始初始化,類的變量初始值準備完成之後,JVM會把該類的方法表也初始化完畢。


方法的執行

解釋執行

在jdk 1.0時代,Java虛擬機完全是解釋執行的,隨着技術的發展,現在主流的虛擬機中大都包含了即時編譯器(JIT)。因此,虛擬機在執行代碼過程中,到底是解釋執行還是編譯執行,只有它自己才能準確判斷了,但是無論什麼虛擬機,其原理基本符合現代經典的編譯原理,如下圖所示: 
 
在Java中,javac編譯器完成了詞法分析、語法分析以及抽象語法樹的過程,最終遍歷語法樹生成線性字節碼指令流的過程,此過程發生在虛擬機外部。

基於棧的指令集與基於寄存器的指令集

Java編譯器輸入的指令流基本上是一種基於棧的指令集架構,指令流中的指令大部分是零地址指令,其執行過程依賴於操作棧。另外一種指令集架構則是基於寄存器的指令集架構,典型的應用是x86的二進制指令集,比如傳統的PC以及Android的Davlik虛擬機。兩者之間最直接的區別是,基於棧的指令集架構不需要硬件的支持,而基於寄存器的指令集架構則完全依賴硬件,這意味基於寄存器的指令集架構執行效率更高,單可移植性差,而基於棧的指令集架構的移植性更高,但執行效率相對較慢,初次之外,相同的操作,基於棧的指令集往往需要更多的指令,比如同樣執行2+3這種邏輯操作,其指令分別如下: 
基於棧的計算流程(以Java虛擬機爲例):

而基於寄存器的計算流程:


基於棧的代碼執行示例

下面我們用簡單的案例來解釋一下JVM代碼執行的過程,代碼實例如下:

使用javap指令查看字節碼:

執行過程中代碼、操作數棧和局部變量表的變化情況如下: 


  1. 也成爲容量槽,虛擬規範中並沒有規定一個Slot應該佔據多大的內存空間。 

  2. 這裏的嚴格匹配指的是字節碼操作的棧中的實際元素類型必須要字節碼規定的元素類型一致。比如iadd指令規定操作兩個整形數據,那麼在操作棧中的實際元素的時候,棧中的兩個元素也必須是整形。 

  3. Animal dog=new Dog();其中的Animal我們稱之爲靜態類型,而Dog稱之爲動態類型。兩者都可以發生變化,區別在於靜態類型只在使用時發生變化,變量本身的靜態類型不會被改變,最終的靜態類型是在編譯期間可知的,而實際類型則是在運行期纔可確定。 

  4. Animal dog=new Dog();其中的Animal我們稱之爲靜態類型,而Dog稱之爲動態類型。兩者都可以發生變化,區別在於靜態類型只在使用時發生變化,變量本身的靜態類型不會被改變,最終的靜態類型是在編譯期間可知的,而實際類型則是在運行期纔可確定。 

  5. 宗量:方法的接受者與方法的參數稱爲方法的宗量。 
    舉個例子: 
    public void dispatcher(){ 
    int result=this.execute(8,9); 

    public void execute(int pointX,pointY){ 
    //TODO 


    在dispatcher()方法中調用了execute(8,9),那此時的方法接受者爲當前this指向的對象,8、9爲方法的參數,this對象和參數就是我們所說的宗量

發佈了58 篇原創文章 · 獲贊 17 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章