JVM字節碼執行引擎

1、概述

  在不一樣的虛擬機實現裏面,執行引擎在執行Java代碼的時候可能會有解釋執行(經過解釋器執行)和編譯器執行(經過即時編譯器產生本地代碼執行)兩種選擇,全部的Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。java

  每一個字節碼指令都由一個1字節的操做碼和附加的操做數組成。程序員

2、運行時棧幀結構

  棧幀(Frame Frame)是用於支持虛擬機運行方法調用和執行的數據結構,每個棧幀都包括了局部變量表、操做數棧、動態連接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時候,棧幀中須要多大的局部變量表,多深的操做數棧都已經徹底肯定了,而且寫入到方法表的Code屬性中。數組

  重點理解:棧幀屬於線程,每一個線程中通常不少方法都同時處於執行狀態。對於執行引擎來說,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀,與之相關聯的方法稱爲當前方法(即每個方法都擁有本身獨立的局部變量表、操做數棧、動態連接和方法的返回地址等信息)。數據結構

  

2.1 局部變量表

  局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序編譯爲Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的局部變量表的最大容量。佈局

  類變量有兩次賦初始值的過程,一次是準備階段,賦予系統初始值,整型 = 0,布爾類型 = false;另外一次是初始化階段,賦予程序員定義的初始值。所以即便在初始化階段程序員沒有爲類變量賦值也沒有關係,類變量仍然具備一個肯定的初始值。可是局部變量不同,若是一個局部變量定義了但沒有賦初始值是不能使用的,字節碼校驗的時候也會被虛擬機發現而致使類加載失敗!性能

 

public static void main(String[] args) {
        int value;
        System.out.println(value);  //程序編譯失敗,未給局部變量附初始值
}

 

2.2 操做數棧

  操做數棧是一個後入先出棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候寫入到方法表的 Code 屬性的 max_locals 數據項中。操做數棧的每個元素能夠是任意的 Java 數據類型,包括 long 和 double;
優化

  當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法執行的過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做。spa

2.3 動態連接

  每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,只有這個引用是爲了支持方法調用過程當中的動態連接(Dynamic Linking)。Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化稱爲靜態連接。另一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態連接!線程

  當前線程的棧幀經過獲取方法的直接引用,指向着常量池對應方法的字節碼,就能夠利用常量池、操做數棧執行方法!blog

2.4 方法返回地址

當一個方法開始執行後,只有兩種方式能夠退出這個方法。

第一種:執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;

第二種:在方法的執行過程當中遇到了異常(Exception),而且議程沒有在方法體中處理,簡稱異常完成出口;

不管何種退出方式,在方法退出以後,都須要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。

3、方法調用

  方法調用並不等於方法執行,方法調用階段惟一的任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程。在程序運行時,進行方法調用是最廣泛、最頻繁的操做,但Class文件的編譯過程當中不包含傳統編譯中的鏈接步驟,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存佈局中的入口地址(至關於上面說的直接引用)。這個特性給Java帶來了更強大的動態擴展能力,但也使得Java方法調用過程變得相對複雜起來,須要在類加載期間,設置到運行期間再能肯定目標方法的直接引用!

3.1 解析

  只要能被invokestatic和invokespecial指令調用的方法,均可以在解析階段中肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器、父類方法4類,它們在類加載的時候就會把符號引用解析爲該方法的直接引用。這些方法能夠稱爲非虛方法。

3.2 靜態分派和動態分派

首先明白一點:Java語言是一種靜態多分派,動態單分派語言!

靜態分派(方法重載關聯)

  變量自己的靜態類型不會被改變,靜態類型在編譯期可知,而實際類型變化的結果在運行期纔可肯定。靜態方法會在類加載期就進行解析,而靜態方法顯然也是能夠擁有重載版本的,選擇重載版本的過程也是經過靜態分派完成的。

動態分派(方法的重寫關聯)

invokevirtual指令的多態查找過程,運行時解析過程分爲:

 

  因爲動態分派是很是頻繁的動做,並且動態分派的方法版本選擇過程須要運行時在類的方法元數據中搜索合適的目標方法,所以在虛擬機的實際實現中基於性能的考慮,大部分實現都不會真正地進行如此頻繁的搜索。面對這種狀況,最經常使用的「穩定優化」手段就是爲類在方法區中創建一個虛方法表(Virtual  Method  Table,也稱爲itable),使用虛擬機表索引來代替元數據以提升性能!

 4、基於棧的字節碼解釋執行引擎

4.1 解釋執行

Java語言常常被人定義爲「解釋執行」的語言!

 

編譯原理的簡單過程:詞法分析 --> 語法分析 --> 語義分析和中間代碼的產生 --> 優化 --> 目標代碼生成!

  Java語言中,javac 編譯器完成了程序代碼通過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的字節碼指令流的過程。這一部分動做是在Java虛擬機以外進行的,而解釋器在虛擬機的內部,因此Java程序的編譯就是半獨立的實現!

4.2 基於棧的指令集

4.3 基於棧的解釋器執行過程

一段簡單的算術代碼:

public int calc(){
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
}

字節碼指令表示:

public int calc();

Code:

Stack=2, Locals=4, Args_size=1 //操做棧深度爲24Slot局部變量表

0bipush 100  //100壓入操做數棧

2istore_1  //將棧頂100數值存放到局變量Slotindex=1

3sipush 200  //200壓入操做數棧

6istore_2     //將棧頂200數值存放到局部變量Slotindex=2

7sipush 300  //300壓入操做數棧

10istore_3  //將棧頂200數值存放到局部變量Slotindex=3

11iload_1  //index=1的局部變量表數值壓入操做數棧(100

12iload_2  //index=2的局部變量表數值壓入操做數棧(200

13iadd  //取棧頂兩個數值相加,結果壓入操做數棧(300

14iload_3  //index=3的局部變量表數值壓入操做數棧(300

15imul       //取棧頂兩個數值相乘,結果壓入操做數棧(90000

16ireturn  //取棧頂數值返回調用者結果

圖解展現:

相關文章
相關標籤/搜索