JVM之字節碼執行引擎

方法調用:java

方法調用不一樣於方法執行,方法調用階段惟一任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不執行方法內部的具體過程。方法調用有,解析調用,分派調用(有靜態分派,動態分派)。架構

方法解析:spa

解析調用必定是一個靜態的過程,在編譯期就徹底肯定,能夠在類加載的解析階段就把涉及的符號引用轉化爲直接引用,不會延遲到運行期再去完成。code

全部方法調用的目標方法在Class文件裏面都是一個常量池中的符號引用,在類加載的解析階段,會將其中一部分符號引用轉化爲直接引用(前提是,方法在程序真正運行前就能夠肯定調用的版本,而且這個方法的調用版本在運行期不會改變,也就是說,調用目標在程序代碼寫好,編譯器編譯時就能肯定下來)。這類方法的調用稱爲解析。對象

編譯期可知,運行期不變,這樣的方法主要有靜態方法和私有方法這兩大類,前者與類型關聯,後者在外部不可被訪問,這兩種方法各自的特色決定了它們都不能經過繼承或別的方式重寫其餘版本,所以它們都適合在類加載階段進行解析。blog

與之對應的是,在Java虛擬機裏面提供了5條調用字節碼指令,有:繼承

invokestatic:調用靜態方法接口

invokespecial:調用實例構造器<init>方法,私有方法和父類方法。內存

invokevirtual:調用全部的虛方法ci

invokeinterface:調用接口方法,會在運行時在肯定一個實現此接口的對象。

只要能被invodestatic,invokespecial指令調用的方法,均可以在解析階段中肯定惟一的調用版本(而invokevirtual,invokeinterface指令調用的方法則不行,由於他們2調用的方法須要去找到子類對應的方法(若是有的話),實現該接口的類的方法,這些都是在運行期肯定的)。能被invokestatic,invokespecial指令對應的方法有靜態方法,私有方法,實例構造器方法,父類方法,它們會在類加載的時候(準確說是在解析階段)就會把符號引用轉化爲直接引用,這些方法稱爲非虛方法,與之對應的稱爲虛方法(final方法除外,即便它是虛方法,可是由於它不能被重寫,能夠在類加載的解析階段就把涉及的符號引用轉化爲直接引用,不會延遲到運行期再去完成)。

分派之靜態分派:

分派調用多是靜態的,也多是動態的,根據分派的宗量數可分爲單分派和多分派。分派調用過程將會揭示多態性特徵的一些基本的體現,如重載,重寫是怎樣實現的。、

變量的類型有靜態類型和實際類型之分,因此在加載器沒法肯定變量的實際類型,也就沒法肯定變量所調用方法的版本。

靜態類型(或者稱外觀類型,C++中稱爲編譯器類型),實際類型(C++中稱爲運行期類型)。這兩個概念是實現多態的基礎。

虛擬機(準確說是編譯器)在重載時是經過參數的靜態類型而不是實際類型做爲斷定依據的,靜態類型是編譯期可知的,由於在編譯階段,Javac編譯器會根據參數的靜態類型決定使用哪一個重載版本。全部依賴靜態類型來定位方法執行版本的分派動做稱爲靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段,所以肯定靜態分派的動做實際上不是由虛擬機來執行的。另外,編譯器雖然能肯定出方法的重載版本,但在不少狀況在這個版本有多個選擇,每每只能肯定一個更加合適的。如,基礎類型變量,以字符變量爲例重載時首選其實際的類型(接觸類型沒有靜態類型這一說),而後是int,long,Character,Serializable,Object.

分派之動態分派:

invokevirtual指令(執行全部的虛方法)的運行時解析過程大體分爲如下幾個步驟:

1.找到操做數棧頂的第一個元素所指向的對象(調用當前方法的對象)的實際類型,記做C

2.若是在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找過程結束;若是不經過拋出java.lang.IllegalAccessError異常;

3.不然,按照繼承關係從下往上依次對C的父類進行第2步的搜索和校驗過程;

4.若是始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

這個過程就是Java語言中方法重寫的本質,在這個過程當中將常量池中的類方法的符號引用轉化爲直接引用(針對於這些虛方法)。咱們把這種在運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派。

單分派與多分派:

方法的接收者與方法的參數統稱爲方法的宗量。單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多於一個宗量對目標方法進行選擇。

對照實例來分析:

 

 1 public class Dispath
 2 {
 3     static class QQ{}
 4 
 5     static class _360{}
 6     
 7     public static class Father {
 8         public void hardChoice(QQ arg){
 9             System.out.println("father choose qq");
10         }
11 
12         public void hardChoice(_360 arg){
13             System.out.println("father choose 360");
14         }
15     }
16 
17     public static class Son extends Father {
18         public void hardChoice(QQ arg){
19             System.out.println("son choose qq");
20         }
21 
22         public void hardChoice(_360 arg){
23             System.out.println("son choose 360");
24         }
25     }
26 
27     public static void main(String[] args){
28         Father father = new Father();
29         Father son = new Son();
30         father.hardChoice(new _360());
31         son.hardChoice(new QQ());
32     }
33 }

 

對於hardChoice()方法

1.編譯器選擇方法版本的過程,也就是靜態分派的過程。這時選擇目標方法的依據有兩點:一點是靜態類型是Father仍是Son,二是方法參數是QQ仍是360。此次選擇結果的產物是產生了兩條invokevirtual指令,兩個指令的參數分別爲常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符號引用。如圖:

 

 

 

 

由於是根據兩個宗量進行選擇,因此Java語言的靜態分派屬於多分派類型。

2.運行階段的選擇,也就是動態分派的過程。在執行「son.hardChoice(new QQ())」這句代碼時,即invokevirtual指令時,因爲編譯期已經決定目標方法的簽名必須是hardChoice(QQ),虛擬機此時不會關心傳進來的參數類型了,由於這時的參數的靜態類型,實際類型已經對方法的選擇不會產生影響了(已經進行了重載,剩下的只有重寫了)。惟一產生影響的是方法調用者的實際類型是Father仍是Son。由於只有一個宗量做爲選擇依據,因此動態分派屬於單分派類型。

 

 

  

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

 許多Java虛擬機的執行引擎在執行Java代碼時都有解釋執行(經過解釋器執行),編譯執行(經過即時編譯器產生本地代碼執行)兩種選擇。

Java編譯器輸出的指令流,基本上是基於棧的指令集架構,指令流中的指令大部分是零地址指令,他們依賴操做棧進行工做。與之對應的是基於寄存器的指令集,如x86二地址指令集(主流PC機使用)。

基於棧的指令集優勢:可移植,代碼想對緊湊(一字節對應一個指令),編譯器實現更加簡單(不用考慮空間分配問題,所需空間在棧上操做)。

缺點:速度慢(由於指令數量多,內存訪問頻繁,訪問棧屬於內存訪問)。

 

基於棧的解釋器執行過程跟處理器執行方式類似。

相關文章
相關標籤/搜索