Java虛擬機的執行引擎輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。java
棧幀是JVM的虛擬機棧中的結構,存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。編譯程序代碼時,棧幀的局部變量表和操做數棧大小已經肯定,寫入到方法表的Code屬性中。所以一個棧幀須要分配多少內存,不會受到程序運行期變量數據的影響,僅僅取決於具體的虛擬機實現。線程中位於虛擬機棧棧頂的棧幀稱爲當前棧幀,對應線程正在執行的當前方法,執行引擎運行的全部字節碼指令都只針對當前棧幀進行操做。棧幀結構:安全
局部變量表用於存放方法參數和方法內定義的局部變量。局部變量表的容量以變量槽(Variable Slot)爲最小單位,虛擬機規範中沒有規定一個Slot應占用的內存空間大小。一個Slot能夠存放一個32位之內的數據類型,其中有boolean、byte、char、short、int、float、reference(這個類型多是32位也多是64位)和returnAddress8種類型。reference類型表示對一個對象實例的引用,虛擬機規範沒有規定它的長度和結構,但虛擬機至少應當經過這個引用作到2點:ide
一、今後引用匯總直接或間接查找到對象在Java堆中的數據存放的起始地址索引。性能
二、此引用中直接或間接的查找到對象所屬數據類型在方法區中存儲的類型信息。優化
Java語言中明確規定64位的數據類型只有long和double,虛擬機會以高位對齊的方式爲其分配2個連續的Slot空間。虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從0開始至局部變量表最大的Slot數量。在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是執行的是實例方法,那局部變量表中第0位索引的Slot默認是用於傳遞方法所屬的對象實例的引用,在方法中經過關鍵字"this"能夠訪問這個隱含的參數。其他參數按照參數列表順序排列,佔用從1開始的局部變量Slot,參數表分配完後,再根據方法內部定義的變量順序和做用域分配其他的Slot。this
爲了儘量節省棧幀空間,局部變量表中的Slot是能夠重用的,若是程序計數器的值已經超出了某個變量的做用域,那這個變量對應的Slot能夠交給其餘變量使用。局部變量不像類變量有個賦零值的階段,所以局部變量在使用前必須手動初始化,不然在編譯時期會報錯。spa
操做數棧也稱爲操做棧,其最大深度在編譯時寫入到Code屬性的max_stacks數據項中,操做數棧中32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。當一個方法開始執行時,這個方法的操做數棧是空的,在方法執行過程當中,字節碼指令會往操做數棧寫入和讀取數據,例如算術運行時經過操做數計算,調用其餘方法的時候經過操做數棧進行參數傳遞。在概念模型中2個棧幀做爲虛擬機的元素是徹底獨立的,但在大多虛擬機的實現中會作一些優化處理,令2個棧幀出現一部分重疊,讓下面棧幀的部分操做數棧與上面棧幀的部分局部變量表重疊在一塊兒,在進行方法調用時就能夠共用一部分數據,不用進行額外的參數賦值傳遞:線程
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令以常量池中指向方法的符號引用做爲參數。這些符號引用一部分在類加載階段或者第一次使用的時候就轉化爲直接引用,這種稱爲靜態解析,另一部分在每一次運行時轉化爲直接引用,這部分稱爲動態鏈接。3d
當一個方法開始執行後,只有2種方式能夠退出。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時可能有返回值傳遞給上層的方法調用者,這種退出方法的方式稱爲正常完成出口。另一種退出方式是,在方法執行中遇到了異常,而且這個異常沒有在方法體內處理,這種退出方法的方式稱爲異常完成出口。異常完成出口的方式退出是不會給上層調用者產生任何返回值的。code
不管採用哪一種退出方式,在方法退出後,都須要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出時,調用者的PC計數器的值能夠做爲返回地址,棧幀中極可能保存這個計數器值。而方法異常退出時,返回地址要經過異常處理器表來肯定,棧幀中通常不保存這部分信息。
方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整PC計數器執行方法調用指令後面的一條指令等。
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀之中。在實際開發中,通常會把動態鏈接、方法返回地址和其餘附加信息所有歸爲一類,稱爲棧幀信息。
方法調用不等同於方法執行,方法調用是肯定被調用方法的版本(即調用哪個方法),暫不涉及方法內部的具體運行過程。
全部方法調用中的目標方法在Class文件中都是一個常量池的符號引用,在類加載階段,會將其中的一部分符號引用轉化爲直接引用,這部分方法須要符合「編譯器可知,運行期不可變」,主要包括靜態方法和私有方法2種,這類方法的調用稱爲解析。
Java虛擬機裏提供了5條方法調用字節碼指令:
一、invokestatic:調用靜態方法。
二、invokespecial:調用實例構造器<init>方法、私有方法和父類方法。
三、invokevirtual:調用全部的虛方法。
四、invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象。
五、invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,而後再執行該方法,前面4條調用指令,分派邏輯是固化在Java虛擬機內部的,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
被invokestatic和invokespecial指令調用的方法,有靜態方法、私有方法、實例構造器、父類方法4類,在類加載的時候就會把符號引用解析爲該方法的直接引用。這些方法方法稱爲非虛方法,其餘的方法稱爲虛方法(final方法屬於非虛方法)。解析調用必定必定是個靜態的過程,在編譯期間就徹底肯定,在類加載的解析階段就會把相應的符號引用所有解析成直接引用。
虛方法是使用分派調用的,分派調用多是靜態也多是動態的,根據分派依據的宗量數可分爲單分派和多分派。解析和分派不是互斥關係,靜態方法在類加載的時候就會解析,但也是能夠重載的。
變量的類型分爲靜態類型和實際類型,靜態類型和實際類型不必定相同,靜態類型是在編譯期可知的,實際類型變化的結果在運行期才能肯定。編譯期在重載時是經過參數的靜態類型而不是實際類型做爲斷定依據的。全部依賴靜態類型來定位方法執行版本的分派動做稱爲靜態分派。靜態分派的典型應用是方法重載。編譯器肯定方法的重載版本,是肯定一個「更加合適的」版本,而不是「惟一的」版本。
1 package com.liu.test; 2 3 public class Test1 { 4 5 static abstract class Human { 6 } 7 8 static class Man extends Human { 9 } 10 11 static class Woman extends Human { 12 } 13 14 public void sayHello(Human human) { 15 System.out.println("Human"); 16 } 17 18 public void sayHello(Man man) { 19 System.out.println("Man"); 20 } 21 22 public void sayHello(Woman woman) { 23 System.out.println("Woman"); 24 } 25 26 public static void main(String[] args) { 27 Human man = new Man(); 28 Human woman = new Woman(); 29 Test1 test = new Test1(); 30 test.sayHello(man); 31 test.sayHello(woman); 32 } 33 }
重載時根據靜態類型斷定,因此2個輸出的都是"Human"。
1 public class Test1 { 2 3 public static void say(Object arg){ 4 System.out.println("Object"); 5 } 6 7 public static void say(int arg){ 8 System.out.println("int"); 9 } 10 11 public static void say(long arg){ 12 System.out.println("long"); 13 } 14 15 public static void say(Character arg){ 16 System.out.println("Character"); 17 } 18 19 public static void say(char arg){ 20 System.out.println("char"); 21 } 22 23 public static void say(char... arg){ 24 System.out.println("char..."); 25 } 26 27 public static void say(Serializable arg){ 28 System.out.println("Serializable"); 29 } 30 31 public static void main(String[] args) { 32 say('a'); 33 } 34 }
運行後輸出"char";若是註釋say(char arg)的方法,那麼輸出"int",這時發生了自動類型轉換。
再去除say(int arg)的方法,輸出"long",這時發生了2次類型轉換,按照char->int->long->float->double的順序轉型進行匹配,但沒有byte和short類型的重載,由於char到byte或short的轉型是不安全的。
繼續註釋掉say(long arg),輸出"Character",這時發生了一次自動裝箱,‘a’被包裝爲它的封裝類型java.lang.Character。
繼續註釋掉say(Character arg),這時會輸出"Serializable",這是由於java.lang.Serializable是java.lang.Character類實現的一個接口,當自動裝箱後發現仍是找不到裝箱類的時候,再次發生了一次自動轉型。char能夠轉型成int,可是Character不會轉型爲Integer,只能安全轉型爲它實現的接口或父類。
繼續註釋掉say(Serializable arg),這時輸出"Object",這時char裝箱後轉型爲父類,根據繼承關係從下往上搜索父類。
當say(Object arg)註釋後,輸出爲"char...",所以變長參數的重載優先級是最低的。
多態中的重載與靜態分派相關,而重寫則與動態分派關聯。動態分派是在運行期根據實際類型確當方法版本的分派過程。
1 public class Test1 { 2 3 static class Human { 4 public void say() { 5 System.out.println("Human"); 6 }; 7 } 8 9 static class Man extends Human { 10 @Override 11 public void say() { 12 System.out.println("Man"); 13 } 14 } 15 16 static class Woman extends Human { 17 @Override 18 public void say() { 19 System.out.println("Woman"); 20 } 21 } 22 23 public static void main(String[] args) { 24 Human man = new Man(); 25 Human woman = new Woman(); 26 man.say(); 27 woman.say(); 28 man = new Woman(); 29 man.say(); 30 } 31 }
輸出:
Man
Woman
Woman
調用invokevirtua指令時進行動態分派,指令的運行時解析過程爲:
一、找到操做數棧頂的第一個元素所指向的對象的實際類型,記做C.
二、若是在類型C中找到與常量中的描述符和簡單名稱都相符的方法,進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找過程結束;若是不經過,返回java.lang.IllegalAccessError異常。
三、不然,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
四、若是始終沒找到合適的方法,拋出java.lang.AbstractMethodError異常。
invokevirtual指令執行的第1步就是在運行期肯定接收者的實際類型,這個過程就是Java語言中方法重寫的本質。
方法的接收者和方法的參數統稱爲方法的宗量,根據這個能夠把分派劃分爲單分派和多分派2種。
動態分派是很是頻繁的動做,並且動態分派的方法版本選擇須要運行時在類的方法元數據中搜索合適的目標方法,所以虛擬機實際實現中基於性能的考慮,須要採用優化策略,最經常使用的是爲類在方法區中創建一個虛方法表,使用虛方法表索引來代替元數據查找。虛方法表中存放着各個方法的實際入口地址,若是方法沒有被重寫,那子類的虛方法表裏的地址入口和父類相同方法的地址入口是一致的,指向父類的實現入口。若是重寫了則子類方法表中的地址會替換爲指向子類實現版本的入口地址。