代碼編譯的結果從本地機器轉變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。編程
執行引擎是Java虛擬機最核心的組成部分之一。虛擬機是一個相對於物理機的概念,這兩種機器都有代碼執行的能力,其區別是物理機的執行引擎是直接創建在處理器、硬件、指令集和操做系統層面上的,而虛擬機的執行引擎則是由本身實現的,所以能夠自行制定指令集與執行引擎的結構體系,而且可以執行那些不被硬件直接支持的指令集格式。數據結構
在Java虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型,這個概念模型成爲各類虛擬機執行引擎的統一外觀。在不一樣的虛擬機實現裏面,執行引擎在執行Java代碼的時候可能會有解釋執行(經過解釋器執行)和編譯執行(經過即時編譯器產生本地代碼執行)兩種選擇,也可能二者兼備,甚至還可能會包含幾個不一樣級別的編譯器執行引擎。可是從外觀上看起來,全部的Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。編程語言
棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。spa
每個棧幀都包括了局部變量表、操做數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時候,棧幀中須要多大的局部變量表、多深的操做數棧都已經徹底肯定了,而且寫入到方法表的Code屬性之中,所以一個棧幀須要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。操作系統
一個線程中的方法調用鏈可能會很長,不少方法都同時處於執行狀態。對於執行引擎來講,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀,與這個棧幀相關聯的方法稱爲當前方法。執行引擎運行的全部字節碼指令都只針對當前棧幀進行操做,典型的棧幀結構以下圖所示:線程
接下來說解棧幀中的局部變量表、操做數棧、動態鏈接、返回地址等各個部分的做用和數據結構。調試
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序編譯爲Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的局部變量表的最大容量。對象
局部變量表的容量以變量槽(Variable Slot)爲最小單位,一個Slot(變量槽)能夠存放一個32位之內的數據類型,Java中佔用32之內的數據類型有boolean、byte、char、short、int、float、reference和 returnAddress 8中類型。對於64位的數據類型,虛擬機會以高位對齊的方式爲其分配兩個連續的Slot空間。同時爲了儘量的節省棧空間,局部變量表的Slot是能夠重用的。但這樣也會形成一個反作用,會直接影響到系統的垃圾收集行爲。blog
操做數棧也常稱爲操做棧,它是一個後入先出(Last In First Out,LIFO)棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候寫入到Code屬性的max_stacks數據項中。繼承
操做數棧的每個元素能夠是任意的Java數據類型,包括long和double。32位的數據類型所佔的棧容量爲1,64位的數據類型所佔的棧容量爲2。在方法執行的任什麼時候候,操做數棧的深度都不會超過max_stacks數據項中設定的最大值。
當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做。
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。
Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化稱爲靜態解析;另一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態鏈接。
當一個方法開始執行後,只有兩種方式能夠退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口。
另一種退出方式是,在方法執行過程當中遇到了異常,而且這個異常沒有在方法體內獲得處理,不管是Java虛擬機內部產生的異常,仍是代碼中使用throw字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種退出方法的方式稱爲異常完成出口。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的。
不管採用何種退出方式,在方法退出以後,都須要返回到方法被調用的位置,這樣程序才能繼續執行。通常來講,方法正常退出時,調用者的PC計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值。而方法異常退出時,返回地址是要經過異常處理器表來肯定的,棧幀中通常不會保存這部分的信息。
方法退出的過程實際上就等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令等。
虛擬機規範容許具體的虛擬機實現增長一些規範中沒有描述的信息到棧幀之中,例如與調試相關的信息,這部分信息徹底取決於具體的虛擬機實現。
在實際的開發中,通常會把動態鏈接、方法返回地址和其餘附加信息所有歸爲一類,稱爲棧幀信息。
方法調用並不等同於方法執行,方法調用階段惟一的任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程。
全部方法調用中的目標方法在Class文件裏面都是一個常量池中的符號引用,在類加載解析階段,會將其中的一部分符號引用轉化爲直接引用,這種解析能成立的前提是:方法在程序真正運行以前就有一個可肯定的調用版本,而且這個方法的調用版本在運行期是不可改變的。換句話說,調用目標在程序代碼寫好、編譯器進行編譯時就必須肯定下來。這類方法的調用稱爲解析。
在Java語言中符合「編譯期可知,運行期不可變」這個要求的方法,主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可被訪問,這兩種方法各自的特色決定了它們都不可能經過繼承或別的方式重寫其餘版本,所以它們都適合在類加載階段進行解析。
與之相對應的是,在Java虛擬機裏面提供了5條方法調用字節碼指令,分別以下:
❤ invokestatic:調用靜態方法;
❤ invokespecial:調用實例構造器<init>方法,私有方法和父類方法;
❤ invokevirtual:調用全部的虛方法:
❤ invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象;
❤ invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,而後再執行該方法,在此以前的4條調用指令,分派邏輯是固化在Java虛擬機內部的,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
只要能被invokestatic和invokespecial指令調用的方法,均可以在解析階段中肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器、父類方法4類,它們在類加載的時候就會把符號引用解析爲該方法的直接引用。這些方法能夠稱爲非虛方法,與之相反,其餘方法稱爲虛方法(除去final方法)。
Java中的非虛方法除了使用invokestatic、invokespecial調用的方法以外還有一種,就是被final修飾的方法。雖然final方法是使用invokevirtual指令來調用的,可是因爲它沒法被覆蓋,沒有其餘版本,因此也無需對方法接收者進行多態選擇,又或者說多態選擇的結果確定是惟一的。因此,final方法是一種非虛方法。
解析調用必定是個靜態的過程,在編譯期間就徹底肯定,在類加載的解析階段就會把涉及的符號引用所有轉變爲可肯定的直接引用,不會延遲到運行期再去完成。
上面的全部內容就是虛擬機如何調用方法的內容。
參考:《深刻理解Java虛擬機》 周志明 編著: