本章將介紹虛擬機如何調用方法
一、java虛擬機字節碼執行引擎
執行引擎在執行代碼的時候可能有解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼執行)兩種。
執行流程:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。
二、運行時棧幀結構
a)用於支持虛擬機進行方法調用和方法執行的數據結構;
b)虛擬機棧的棧元素;
c)存儲了方法的局部變量表、操作數棧、動態鏈接和方法返回地址等信息。
d)每一個方法從調用開始到執行完成的過程,就對應着一個棧幀在虛擬機裏面從入棧到出棧的過程。
e)編譯時,棧幀需要多大的局部變量表、多深的操作數棧都已確定,寫入至Code屬性之中,一個棧幀需要分配多少內存不受運行期變量數據的影響。
f)活動線程中只有棧頂的棧幀(當前棧幀)是有效的。
2.1 局部變量表
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。
最小單位:變量槽(Variable Slot ),支持存放boolean、byte、char、short、int、float、reference或returnAddress類型的數據。
局部變量沒有所謂的“準備階段”,相比較於來變量來說,類變量有兩次賦值的過程,一次是準備階段,賦予系統初始值,另一種是初始化階段,賦予程序員定義的初始值。局部變量程序員沒有賦予初始值的話是不能使用的。
2.2 操作數棧
後入先出。
方法執行過程中,字節碼指令向操作數棧中寫入和提取內容,也就是入棧和出棧操作。
2.3動態連接
每個棧幀都包含一個指向運行時常量池中該棧幀所述方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用-靜態解析。另一部分將在每次運行期間轉化爲直接引用,這部分稱爲動態連接。
2.4 方法返回地址
方法退出方式:a)正常完成出口---執行引擎遇到任意一個發那個發返回的字節碼指令,b)方法執行過程中遇到異常。
退出的過程實際等同於當前棧幀出棧,因此退出的操作有:
1.恢復上層方法的局部變量表和操作數棧
2.把返回值壓人調用者棧幀的操作數棧中
3.調整PC計數器的值以指向方法調用指令後面的一條指令。
三、方法調用
方法調用不等同於方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪個方法)
3.1 解析
所有方法調用中的目標方法在Class文件裏面都是一個常量池中的符號引用。
前提:方法在程序真正運行之前就有一個可確定的調用版本,並且這個方法的調用版本在運行期是不可改變的,換句話說,調用目標在程序代碼寫好,編譯器進行編譯時就 必須確定下來。
適用:靜態方法和私有方法。前者與類型直接關聯,後者在外部不可被訪問。
Java虛擬機裏面提供了四條方法調用字節碼指令:
a)invokestatic:調用靜態方法。
b)invokespecial:調用實例構造器<init>方法、私有方法和父類方法。
c)invokevirtual:調用所有的虛方法。
d)invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。
符合a和b這兩個條件的,都可以在解析前確定唯一的調用版本,符合這個條件的有靜態方法,私有方法,實例構造器和父類方法四類。
解析調用一定是一個靜態的過程,在編譯期間就完全確定,在累裝載的解析階段就會把涉及的符號引用全部轉變爲可確定的直接引用,不會延遲到運行期再去完成。
3.2 分派
public class StaticTest {
static abstract class Humen{
}
static class Men extends Humen{
}
static class Women extends Humen{
}
public void sayHello(Humen h ){
System.out.println(" Humen ");
}
public void sayHello(Men m){
System.out.println(" Men ");
}
public void sayHello(Women w){
System.out.println(" Women ");
}
public static void main(String[] args) {
StaticTest st = new StaticTest();
Humen h = new Men();//等號前爲靜態類型 等號後面爲實際類型
Humen hh = new Women();//等號前爲靜態類型 等號後面爲實際類型
st.sayHello(h);
st.sayHello(hh);
}
}
運行結果:
Humen
Humen
爲什麼呢?
Humen h = new Men();//等號前爲靜態類型 等號後面爲實際類型
靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會被改變,並且最終的靜態類型在編譯期可知的;
實際類型變化的結果在運行期纔可確定,編譯器在編譯程序的時候並不知道一個對象的實際類型是什麼。
針對這樣的輸出結果:編譯器在重載時通過參數的靜態類型決定使用那個重載版本。因爲在編譯階段,靜態類型是可知的。
3.2.1靜態分派--重載
依賴靜態類型來定位方法執行版本的分派動作。最典型的應用就是方法重載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。
編譯器確定出方法的重載版本在很多情況下都不是唯一的,只能確定出方法“更加適合的版本”。
3.2.2動態分派--重寫
在運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。
動態分派查找過程:
1)找到操作棧頂的第一個元素所指向懂得對象的實際類型,記作C;
2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限檢驗,如果通過則返回這個方法的直接引用,查找過程結束。不通過則返回java.lang.IllegalAccessErrir異常。
3)否則,按照繼承關係從下往上一次對C的各個父類進行第2步的搜索和驗證過程。
4)如果始終沒有找到合適的方法,則拋出java.lang,AbstractMethodError異常。
3.2.3單分派和多分派
靜態分派屬於多份派;
動態分派屬於單分派;
宗量:方法的接收者和方法的參數統稱。
public class Zongliang {
static class QQ{}
static class T360{}
static class Father{
void chooice(QQ q){
System.out.println("Father choice QQ");
}
void chooice(T360 q){
System.out.println("Father choice T360");
}
}
static class Son extends Father{
void chooice(QQ q){
System.out.println("Son choice QQ");
}
void chooice(T360 q){
System.out.println("Son choice T360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.chooice(new QQ());
son.chooice(new T360());
}
}
運行結果:
Father choice QQ
Son choice T360
分析:進行靜態分派時(編譯階段編譯器的選擇過程),依據等號左邊 可匹配到靜態類型是Father,等號右邊方法參數是QQ和360,這次選擇的最終產物是兩條invokevirtual指令,兩條指令的參數分別爲常量池中指向Father.choice(T360)和Father.choice(QQ)方法的符號引用,這裏的依據是有兩個宗量;
再看看運行階段虛擬機的選擇,即動態分派的過程,在執行 son.chooice(new T360());時,更準確的是執行這句代碼所對應的invokevirtual指令時,由於編譯期已經決定目標方法的簽名必須爲son.chooice(new T360());唯一可以影響虛擬機選擇的因素只有此方法的接收者的實際類型是Father還是son(具體執行方法的對象稱爲接收者,等號右邊爲接收者)。因此只有一個宗量可以選擇。
3.2.4虛擬機動態分派的實現
動態分派的方法版本選擇過程需要運行時在類的方法元數據中搜索合適的目標方法,基於性能考慮,穩定優化:在類的方法區中建立一個虛方法表,與此對應,在invokeinterface執行時也會用到接口方法表,使用虛方法表索引代替元數據查找以提高性能。
四、基於棧的字節碼解釋執行引擎
虛擬機如何執行方法裏面的字節碼指令的?
。。。我也不知道!