一般一部賽車的引擎是賽車的心臟,決定着賽車的性能和穩定性,賽車的速度、操縱感這些直接與車手相關的指標都是創建在引擎的基礎上的。一樣的,JVM的執行引擎是JAVA虛擬機最核心的組成部分之一。那麼什麼是JVM的執行引擎?咱們在學習計算機組成原理等課程的時候,知道物理機的執行引擎是直接創建在處理、硬件、指令集和操做系統層面上的。而相對於物理機,JAVA虛擬機一樣具備代碼執行的能力,虛擬機的執行引擎是由本身實現的,所以能夠自行定製指令集和執行引擎的結構體系。java
在JAVA虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型,儘管如今JVM的實現各不相同,有編譯執行(如BEA JRockit)也有解釋執行(如Sun Classic VM),可是從概念模型的角度來看,全部JAVA虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。web
1、運行時棧幀(Stack Frame)結構數組
咱們知道程序、指令在運行的時候少不了計算機存儲、組織的方式,這種存儲和組織方式咱們也稱爲數據結構。棧幀就是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。每個棧幀都包括了局部變量表、操做數棧、動態鏈接、方法返回地址以及一些額外的附加信息。數據結構
一個線程中的方法調用鏈可能會很長,不少方法都處於執行狀態。可是對於執行引擎來講,活動線程中只有棧頂的棧幀是有效的,稱爲當前棧幀,對應的方法爲當前方法。執行引擎的全部字節碼指令都是隻針對當前棧幀進行操做的。架構
圖 1. 棧幀的概念結構jvm
棧幀須要多大的局部變量表、多深的操做數,在代碼編譯的時候就已經徹底肯定下來了,寫在了方法表的Code屬性之中,例如:ide
1 public int add(int i,int j){ 2 int result; 3 result = i + j; 4 return result; 5 }
將上術方法所在java文件編譯以後生成的class文件,經過命令 javap -verbose反編譯以後add方法所對應的字節碼以下:佈局
1 public int add(int, int); 2 Code: 3 Stack=2, Locals=4, Args_size=3 4 0: iload_1 5 1: iload_2 6 2: iadd 7 3: istore_3 8 4: iload_3 9 5: ireturn 10 LineNumberTable: 11 line 7: 0 12 line 8: 4 13 14 LocalVariableTable: 15 Start Length Slot Name Signature 16 0 6 0 this Ljvm/executionengine/stackframe/StackFrame; 17 0 6 1 i I 18 0 6 2 j I 19 4 2 3 result I
在上面的字節碼中能夠看到,方法的Code屬性中會有這三個值Stack=2, Locals=4, Args_size=3,其中Stack和Locals就是操做數棧的最大深度以及局部變量表的大小,最後一個參數的個數。細心的朋友會發現局部變量明明只有3個(i,j,result)佔用3個Slot(後面會講到Slot),參數個數明明也只有2個(i,j),爲何都會多一個呢?這裏面咱們不要忘記了非靜態方法都會有個隱藏的變量(參數),那就是「this」。性能
(1) 局部變量表學習
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的最大局部變量表的容量。
局部變量表的容量以變量槽(Slot)爲最小單位,JAVA虛擬機規範中並無指明一個Slot應占用的內存空間大小,只是說明一個Slot都應該能存放一個32位之內的數據類型,有boolean、byte、char、short、int、float、reference(長度與實際使用的是32位仍是64位虛擬機有關)和returnAddress類型的數據。 returnAddress類型是爲字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。long和double是java語言中明確規定的64位數據類型,虛擬機會爲其分配兩個連續的Slot空間。
虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是是實例方法(非static),那麼局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中經過this訪問。其他參數則按照參數表的順序來排列,佔用從1開始的局部變量Slot,參數表分配完畢以後,再根據方法體內部定義的變量順序和做用域分配其他的Slot。
下面經過一個簡單的例子來講明上述的Slot的描述:
1 public int slot(int i,int j){ 2 double d = 5.23; 3 int result; 4 result = i + j; 5 return result; 6 }
1 public int slot(int, int); 2 Code: 3 Stack=2, Locals=6, Args_size=3 4 0: ldc2_w #16; //double 5.23d 5 3: dstore_3 6 4: iload_1 7 5: iload_2 8 6: iadd 9 7: istore 5 10 9: iload 5 11 11: ireturn 12 LineNumberTable: 13 line 6: 0 14 line 8: 4 15 line 9: 9 16 17 LocalVariableTable: 18 Start Length Slot Name Signature 19 0 12 0 this Ljvm/executionengine/stackframe/StackFrame; //this佔用slot0 20 0 12 1 i I //int型 i 佔用slot1 21 0 12 2 j I //int型 j 佔用slot2 22 4 8 3 d D // double型 d 佔用slot3 slot4 23 9 3 5 result I //int型 result 佔用slot5
Slot是能夠重用的,當Slot中的變量超出了做用域,那麼下一次分配Slot的時候,將會覆蓋原來的數據。Slot對對象的引用會影響GC(要是被引用,將不會被回收)。系統不會爲局部變量賦予初始值(實例變量和類變量都會被賦予初始值),也就是說不存在類變量那樣的準備階段。爲了說明Slot的可複用,下面舉了一個在實際中基本不會出現的代碼:
1 public void slotReuse(){ 2 { 3 int m = 100; 4 } 5 int n = 200; 6 }
1 public void slotReuse(); 2 Code: 3 Stack=1, Locals=2, Args_size=1 //變量個數3個(this,m,n),可是局部變量表大小爲2 4 0: bipush 100 5 2: istore_1 //把m的值100放入下標1的slot中 6 3: sipush 200 7 6: istore_1 //把n的值200放入下標1的slot中,超出了m做用域,複用了 8 7: return 9 LineNumberTable: 10 line 7: 0 11 line 9: 3 12 line 10: 7 13 14 LocalVariableTable: 15 Start Length Slot Name Signature 16 0 8 0 this Ljvm/executionengine/stackframe/StackFrame; 17 7 1 1 n I
Slot的複用在某些狀況下會直接影響到系統的垃圾回收行爲,咱們來經過下面的3塊代碼來講明這個問題:
1 /** 2 * 三段代碼加上虛擬機的運行參數「-verbose:gc」來觀察垃圾收集的過程 3 */ 4 /*代碼一:gc的時候,變量placeHolder還處於做用域,沒有回收掉*/ 5 public static void main(String[] args) { 6 byte[] placeHolder = new byte[64*1024*1024]; 7 System.gc(); 8 } 9 /*gc過程: 10 [GC 66167K->65824K(120576K), 0.0012810 secs] 11 [Full GC 65824K->65690K(120576K), 0.0060200 secs] 12 */ 13 14 /*代碼二:gc的時候,變量placeHolder還處於做用域以外了,可是仍是沒有回收掉*/ 15 public static void main(String[] args) { 16 { 17 byte[] placeHolder = new byte[64*1024*1024]; 18 } 19 System.gc(); 20 } 21 /*gc過程: 22 [GC 66167K->65760K(120576K), 0.0013770 secs] 23 [Full GC 65760K->65690K(120576K), 0.0055290 secs] 24 */ 25 26 /*代碼三:gc的時候,變量placeHolder還處於做用域以外了,經過一個新的變量i賦值,複用掉placeHolder原先的slot,垃圾回收了*/ 27 public static void main(String[] args) { 28 { 29 byte[] placeHolder = new byte[64*1024*1024]; 30 } 31 int i = 0; 32 System.gc(); 33 } 34 /*gc過程: 35 [GC 66167K->65760K(120576K), 0.0017850 secs] 36 [Full GC 65760K->154K(120576K), 0.0064340 secs] 37 */
上述代碼中, placeHolder可否被回收的根本緣由就是:局部變量表中的Slot是否還存在關於placeHolder數組對象的引用。代碼二中雖然已經離開了placeHolder的做用域,可是此後沒有局部變量表的讀寫操做,placeHolder本來佔用的Slot尚未被其餘變量複用,因此在GC的時候仍然保留有對數組對象的引用。代碼三中,認爲的定義的一個局部變量i,而且賦值以達到複用剛纔placeHolder的Slot,消除了對數組對象的引用,而後GC就能夠回收掉了。
(2) 操做數棧
Java虛擬機的解釋執行引擎被稱爲"基於棧的執行引擎",其中所指的棧就是指-操做數棧。操做數棧也常被稱爲操做棧,就是一個棧結構(後入先出)。同局部變量表同樣,操做數的最大深度在編譯的時候就肯定了,寫在了方法的Code屬性的max_stacks數據項中。當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法執行的執行過程當中,會有各類字節碼指令向操做數棧中寫入和提取數據(入棧、出棧操做)。
舉個例子:整數加法的字節碼指令iadd在運行以前要求操做數棧中最接近棧頂的兩個元素已經存有兩個int型數值,當指定iadd時候,會將這兩個int值出棧相加,而後將結果入棧。
1 /*java文件中add方法*/ 2 public int add(int i,int j) { 3 return i+j; 4 } 5 6 /*class文件反編譯以後的add方法字節碼*/ 7 public int add(int, int); 8 Code: 9 Stack=2, Locals=3, Args_size=3 10 0: iload_1 //將slot1的數值入棧 變量i的值 11 1: iload_2 //將slot2的數值入棧 變量j的值 12 2: iadd //將棧中的兩個值相加,併入棧 13 3: ireturn 14 LineNumberTable: 15 line 17: 0 16 17 LocalVariableTable: 18 Start Length Slot Name Signature 19 0 4 0 this Ljvm/learn/workspace/SlotGC; 20 0 4 1 i I 21 0 4 2 j I
另外,在虛擬機概念模型中,兩個棧幀做爲虛擬機棧的元素,相互之間是徹底獨立的。可是大多數虛擬機的實現裏都會作一些優化處理,令兩個棧幀出現一部分重疊。讓下面棧幀的部分操做數棧與上面棧幀的部分局部變量表重疊在一塊兒,這樣在方法調用的時候能夠共用一部分數據,減小了額外的參數複製傳遞的開銷。
(3) 動態鏈接
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。咱們知道字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數,這些符號引用一部分會在類加載階段或者第一次使用的時候化爲直接引用,這話總轉化叫作靜態解析,還有一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態鏈接。
(4) 方法返回地址
當一個方法被執行後,有兩種方式退出這個方法:1.執行引擎遇到任意方法返回的字節碼指令,這時可能會返回值傳遞給上層的方法調用者,是否有返回值和返回值的類型將根據遇到何種方法返回指令決定。這種退出方式爲正常完成出口。2.遇到異常而且沒有在方法體內獲得處理,不管是Java虛擬機內部產生異常仍是使用athrow字節碼指令產生異常,只要在本方法的異常表中沒有搜到匹配的異常處理器,就會致使方法退出,這種退出方式是不會給它的上層調用者產生任何返回值的。這種退出方式爲異常完成出口。
通常來講,方法正常退出時,調用者的PC計數器的值就能夠做爲返回地址,棧幀中極可能會保存這個計數器值。而方法異常退出時,返回地址是要經過異常處理器表來肯定的,棧幀中通常不會保存這部分信息。方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表盒操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令等。
(5) 附加信息
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀之中,例如調試相關信息。
2、方法調用
方法調用並不等同於方法執行,方法調用階段惟一的任務就是肯定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程。Class文件的編譯過程當中不包含傳統編譯中的鏈接步驟,一切方法調用在Class文件中都是符號引用,而不是方法在實際運行時內存佈局中的入口地址(直接引用)。這個特色給Java帶來了更強大的動態擴展能力,可是也帶來了複雜,須要在類加載甚至運行期間才能肯定目標方法的直接引用。
在類加載的解析階段,會將其中一部分符號引用轉化爲直接引用,不會延遲到運行期去完成,這種方法的調用稱爲解析(Resolution)。解析能成立的前提是:方法在程序真正運行以前就有可肯定調用版本,而且在運行期間不可變。分派(Dispatch)調用則多是靜態的也多是動態的,根據分派依據的宗量數可分爲單分派和多分派,所以分派有四狀況:靜態單分派、靜態多分派、動態單分派、動態多分派。
(1) 解析
Java虛擬機裏面有四條主要的方法調用字節碼指令:
1.invokestatic:調用靜態方法
2.invokespecial:調用實例構造器<init>方法、私有方法和父類方法
3.invokevirtual:調用全部虛方法
4.invokeinterface:弟阿勇接口方法,會在運行時再肯定一個實現此接口的對象
此外,JSR-292中引入了第5條新的字節碼指令invokedynamic,在這裏不作討論。
只要能被invokestatic和invokespecial指令調用的方法,均可以在解析階段肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器和父類方法。此外,final方法也是能夠在解析階段肯定惟調用版本。
1 /** 2 * 方法靜態解析演示 3 */ 4 //Hello.java 5 public class Hello { 6 public void sayHello(){ 7 System.out.println("hello jvm"); 8 } 9 } 10 //StackFrame.java 11 public class StackFrame { 12 13 public static void staticInvoke(){ 14 System.out.println(); 15 } 16 public void showInvoke(){ 17 staticInvoke(); 18 Hello hello = new Hello(); 19 hello.sayHello(); 20 } 21 } 22 23 //class文件反編譯後showInvoke方法的字節碼 24 public void showInvoke(); 25 Code: 26 Stack=2, Locals=2, Args_size=1 27 0: invokestatic #27; //Method staticInvoke:()V 28 3: new #29; //class jvm/executionengine/stackframe/Hello 29 6: dup 30 7: invokespecial #31; //Method jvm/executionengine/stackframe/Hello."<init>":()V 31 10: astore_1 32 11: aload_1 33 12: invokevirtual #32; //Method jvm/executionengine/stackframe/Hello.sayHello:()V 34 15: return 35 LineNumberTable: 36 line 15: 0 37 line 16: 3 38 line 17: 11 39 line 18: 15 40 41 LocalVariableTable: 42 Start Length Slot Name Signature 43 0 16 0 this Ljvm/executionengine/stackframe/StackFrame; 44 11 5 1 hello Ljvm/executionengine/stackframe/Hello;
(2) 分派
1.靜態分派
1 public class StaticDispatch { 2 public class Human{ 3 4 } 5 public class Man extends Human{ 6 7 } 8 public class Women extends Human{ 9 10 } 11 public void sayHello(Human human){ 12 System.out.println("hello,human"); 13 } 14 public void sayHello(Man man){ 15 System.out.println("hello,man"); 16 } 17 public void sayHello(Women women){ 18 System.out.println("hello,women"); 19 } 20 public static void main(String[] args) { 21 //man的靜態類型是Human,實際類型是Man 22 Human man = new StaticDispatch().new Man(); 23 //man的靜態類型是Human,實際類型是Women 24 Human women = new StaticDispatch().new Women(); 25 StaticDispatch sd = new StaticDispatch(); 26 27 sd.sayHello(man); //輸出:hello,human 28 sd.sayHello(women); //輸出:hello,human 29 sd.sayHello((Man)man); //輸出:hello,man 30 sd.sayHello((Women)women); //輸出:hello,women 31 }
全部依賴靜態類型來定位方法執行版本的分派動做,都稱爲靜態分派。靜態分派最典型的應用就是方法重載。靜態分派放生在編譯階段,所以肯定靜態分派的動做實際上不是有虛擬機來執行的。編譯器能肯定出方法的重載版本,而且這種重載版本不是惟一的,每每只能肯定一個「更加合適」的版本。
1 public class Overload { 2 3 public static void sayHello(int i){ 4 System.out.println("hello int"); 5 } 6 public static void sayHello(long i){ 7 System.out.println("hello long"); 8 } 9 public static void main(String[] args) { 10 //發生了一次自動轉換,自動轉int(相比於long更合適) 11 sayHello('a'); //輸出:hello int 12 } 13 }
2.動態分派
1 public class DynamicDispatch { 2 3 static abstract class Human{ 4 protected abstract void sayHello(); 5 } 6 static class Man extends Human{ 7 protected void sayHello(){ 8 System.out.println("hello,man"); 9 } 10 } 11 static class Women extends Human{ 12 protected void sayHello(){ 13 System.out.println("hello,women"); 14 } 15 } 16 public static void main(String[] args) { 17 Human man = new Man(); 18 Human women = new Women(); 19 man.sayHello(); //輸出:hello,man 20 women.sayHello();//輸出:hello,women 21 } 22 23 }
顯然也不是靜態類型決定的,從上面代碼能夠看到,靜態類型代碼也都是Human,可是最後執行的結果倒是不一樣的。致使的緣由就是這個兩個變量的實際類型不一樣,Java虛擬機是如何根據實際類型來分派呢?咱們使用javap來查看一下字節碼。
1 public static void main(java.lang.String[]); 2 Code: 3 Stack=2, Locals=3, Args_size=1 4 0: new #16; //class jvm/executionengine/stackframe/DynamicDispatch$Man 5 3: dup 6 4: invokespecial #18; //Method jvm/executionengine/stackframe/DynamicDispatch$Man."<init>":()V 7 7: astore_1 8 8: new #19; //class jvm/executionengine/stackframe/DynamicDispatch$Women 9 11: dup 10 12: invokespecial #21; //Method jvm/executionengine/stackframe/DynamicDispatch$Women."<init>":()V 11 15: astore_2 12 16: aload_1 13 17: invokevirtual #22; //Method jvm/executionengine/stackframe/DynamicDispatch$Human.sayHello:()V 14 20: aload_2 15 21: invokevirtual #22; //Method jvm/executionengine/stackframe/DynamicDispatch$Human.sayHello:()V 16 24: return 17 LineNumberTable: 18 line 19: 0 19 line 20: 8 20 line 21: 16 21 line 22: 20 22 line 23: 24 23 24 LocalVariableTable: 25 Start Length Slot Name Signature 26 0 25 0 args [Ljava/lang/String; 27 8 17 1 man Ljvm/executionengine/stackframe/DynamicDispatch$Human; 28 16 9 2 women Ljvm/executionengine/stackframe/DynamicDispatch$Human;
字節碼中0-15的字節碼都是new了兩個實例,並將實例放入第一個和第二個Slot中,接下來16-17是將Slot1中值壓入棧(man實例的引用),而且調用方法,20-21是將Slot2中值壓入棧(women實例的引用),而且調用方法。單從17和21兩行來看,調用方法符號引用如出一轍,可是這兩條指令最終執行的目標方法確實不一樣。緣由就跟invokevirtual指令的多態查找過程有關了,invokevirtual指令的運行時解析過程大體分爲如下步驟:
1) 找到操做數棧頂的第一個元素所指對象的實際類型,記做C。
2) 若是在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,經過就直接引用,結束;不經過就返回java.lang.IllegalAccessError異常。
3) 不然,按照繼承關係從下往上對C的各個父類重複第2步的搜索和校驗。
4) 若是始終沒找到合適的方法,則拋java.lang.AbstractMethodError異常。
從上面invokevirtual指令的運行時解析過程不難看出,代碼中man和woman會找到實際類型中的方法調用。這個過程反映了java語言中方法重寫的本質。
3.單分派和多分派
方法的接收者與方法的參數統稱爲方法的宗量。根據分派基於多少種宗量,能夠將分派分爲單分派和多分派。單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多於一個宗量對目標方法進行選擇。
1 /** 2 * 單分派、多分派 3 */ 4 public class Dispatch { 5 static class QQ{}; 6 7 static class _360{}; 8 9 public static class Father{ 10 public void choice(QQ arg) { 11 System.out.println("father choose QQ"); 12 } 13 public void choice(_360 arg) { 14 System.out.println("father choose 360"); 15 } 16 } 17 public static class Son extends Father{ 18 public void choice(QQ arg) { 19 System.out.println("son choose QQ"); 20 } 21 public void choice(_360 arg) { 22 System.out.println("son choose 360"); 23 } 24 } 25 26 public static void main(String[] args) { 27 Father father = new Father(); 28 Father son = new Son(); 29 //動態類型Father 靜態類型_360 根據方法接收者:Father 和 方法參數:_360 肯定一個目標方法 30 father.choice(new _360()); //輸出:father choose 360 31 ////動態類型Son 靜態類型QQ 根據方法接收者:Son 和 方法參數:QQ 肯定一個目標方法 32 son.choice(new QQ()); //輸出:son choose QQ 33 } 34 }
4.虛擬機動態分派的實現
因爲動態分派很是繁瑣以及虛擬機實際實現中基於性能考慮,一般都會對動態分派的實現作優化。最一般的優化方法就是在類的方法區中建一個虛方法表(Virtual Method Talbe,vtable),於此對應,invokeinterface執行時也用到接口方法表(Interface Method Table,itable)。
圖2 方法表結構
虛方法表中存放着各個方法的實際入口地址。若是某個方法在子類中沒有被重寫,那麼子類的虛方法表中的地址入口地址與父類相同方法的地址入口地址一致,都指向父類的實現入口。若是子類重寫了這個方法,那麼子類虛方法表中地址將會被替換成指向子類實現版本的入口地址。上圖中Son重寫了來自Father的所有方法,所以Son方法表中這些方法的實際入口地址都指向了Son類型數據的方法。Son和Father都沒有重寫Object中的方法,因此方法表中的實際入口地址都指向了Object數據類型。
爲了程序實現上的方便,具備相同簽名的方法,在父類、子類的虛方法表中都應當具備同樣的索引序號,這樣當類型變換時,僅須要變動查找的方法表,就能夠從不一樣的虛方法表中按索引轉換出所需的入口地址。
方法表通常在類加載的鏈接階段進行初始化,準備了類的變量初始值後,虛擬機會把該類的方法表也初始化完畢。
3、基於棧的字節碼解釋執行引擎
Java編譯器輸出的指令流,基本上是一種基於棧的指令集架構,它們依賴操做數棧進行工做。下面結合一個小例子看看虛擬機其實是如何執行的。
1 //java文件中的一個計算方法 2 public int showExample(){ 3 int a = 10; 4 int b = 20; 5 int c = 30; 6 return a*(b+c); 7 } 8 9 10 //class文件中 showExample方法的字節碼 11 public int showExample(); 12 Code: 13 Stack=3, Locals=4, Args_size=1 14 0: bipush 10 15 2: istore_1 16 3: bipush 20 17 5: istore_2 18 6: bipush 30 19 8: istore_3 20 9: iload_1 21 10: iload_2 22 11: iload_3 23 12: iadd 24 13: imul 25 14: ireturn 26 LineNumberTable: 27 line 5: 0 28 line 6: 3 29 line 7: 6 30 line 8: 9 31 32 LocalVariableTable: 33 Start Length Slot Name Signature 34 0 15 0 this Ljvm/executionengine/stackframe/example; 35 3 12 1 a I 36 6 9 2 b I 37 9 6 3 c I