執行引擎是 Java 虛擬機最核心的組成部分之一。在不一樣的虛擬機實現裏,執行引擎在執行 Java 代碼時可能會有解釋執行(經過解釋器執行)和編譯執行(經過即時編譯器產生本地代碼執行)兩種選擇,也可能二者兼備。但從外觀上看,全部 Java 虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。數據結構
物理機與虛擬機的執行引擎:架構
棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接、方法返回地址和一些額外的附加信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機裏面從入棧到出棧的過程。性能
對於執行引擎來講,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀,與這個棧幀相關聯的方法稱爲當前方法。執行引擎運行的全部字節碼指令都只針對當前棧幀進行操做。優化
局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。this
局部變量表的容量以變量槽(Variable Slot,下稱 Slot)爲最小單位,虛擬機規範中並無明確指明一個 Slot 應占用的內存空間大小。爲了儘量節省棧幀空間,局部變量表中的 Slot 是能夠重用的。操作系統
虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從 0 開始至局部變量表最大的 Slot 數量。線程
在方法執行時,虛擬機經過局部變量表完成參數值到參數變量列表的傳遞。若是執行的是實例方法(非 static 方法),那局部變量表中第 0 位索引的 Slot 默認用於傳遞方法所屬對象實例的引用,在方法中可經過關鍵字「this」訪問到這個隱含的參數。其他參數則按照參數表順序排列,參數表分配完畢後,再根據方法體內部定義的變量順序和做用域分配其他的 Slot。調試
操做數棧(Operand Stack)也稱爲操做棧,它是一個後入先出的棧。操做數棧的每個元素能夠是任意的 Java 數據類型。對象
當一個方法剛開始執行時,這個方法的操做數棧是空的,在方法執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,這就是出棧/入棧操做。blog
在概念模型中,兩個棧幀做爲虛擬機棧的元素,是徹底獨立的。但在大多數虛擬機的實現裏會作一些優化處理,令兩個棧幀出現一部分重疊。這樣在進行方法調用時就能夠共用一部分數據,無須進行額外的參數複製傳遞。
每一個棧幀都包含一個指向運行時常量池中,該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)。
兩種退出方法的方式:
不管何種退出方式,方法退出後都須要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。
通常來講,方法正常退出時,調用者的 PC 計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值。而方法異常退出時,返回地址要經過異常處理器表來肯定,棧幀中通常不會保存這部分信息。
方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(有的話)壓入調用者棧幀的操做數棧中,調整 PC 計數器的值以指向方法調用指令後面的一條指令等。
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀中,例如與調試相關的信息。
方法調用並不等於方法執行,方法調用階段惟一的任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程。
一切方法調用在 Class 文件裏存儲的只是符號引用,而不是直接引用,只有在類加載期間,甚至是運行期間才能肯定目標方法的直接引用。
方法調用字節碼指令:
在類加載的解析階段,將方法的符號引用轉化爲直接引用,這類方法調用稱爲解析。這種解析能成立的前提是:方法在程序執行以前有一個可肯定的調用版本,而且這個方法的調用版本在運行期不可改變,即「編譯期可知,運行期不可變」。
只要能被 invokestatic 和 invokespecial 指令調用的方法,均可以在解析階段肯定惟一的調用版本,所以都能在類加載階段被解析。這些方法稱爲非虛方法,與之相反,其餘方法稱爲虛方法。
final 方法雖然是使用 invokevirtual 指令調用的,但因爲它沒法被覆蓋,沒有其餘版本,因此無須對方法接收者進行多態選擇。所以,fanal 方法也屬於非虛方法。
依賴靜態類型(又稱外觀類型)來定位方法執行版本的分派動做,稱爲靜態分派。靜態分派的典型應用是方法重載。
靜態類型是編譯期可知的。
靜態分派發生在編譯階段,所以肯定靜態分派的動做不是由虛擬機來執行的。
在運行期根據實際類型肯定方法執行版本的分派過程,稱爲動態分派。動態分派的典型應用是方法重寫。
實際類型是在運行期纔可肯定。
動態分派是很是頻繁的動做,並且運行時須要在類的方法元數據中搜索合適的目標方法。基於性能的考慮,大部分的虛擬機實現都不會真正地進行如此頻繁的搜索。最經常使用的「穩定優化」手段是爲類在方法區中創建一個虛方法表,使用虛方法表索引來代替元數據查找以提升性能。
虛方法表中存放着各個方法的實際入口地址。
根據分派基於多少種宗量,能夠將分派劃分爲單分派和多分派。單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多於一個宗量對目標方法進行選擇。
方法的接收者與方法的參數統稱爲方法的宗量。
靜態分派是根據方法接收者的靜態類型和方法參數來選擇目標方法的,所以靜態分派屬於多分派類型。
動態分派只根據方法接收者的實例類型來選擇目標方法,所以動態分派屬於單分派類型。
現在,基於物理機、虛擬機的語言,大多都會遵循基於現代經典編譯原理的思路,在執行前先對程序源碼進行詞法分析和語法分析處理,把源碼轉化爲抽象語法樹。
對於一門具體語言的實現來講,詞法分析、語法分析以及後面的優化器和目標代碼生成器均可以選擇獨立於執行引擎,造成一個完整意義的編譯器去實現,這類表明是 C/C++ 語言。也能夠選擇把其中的一部分(如生成抽象語法樹以前的步驟)實現爲一個半獨立的編譯器,這類表明是 Java 語言。又或者把這些步驟和執行引擎所有集中封裝在一個封閉的黑匣子之中,如大多數的 JavaScript 執行器。
Java 語言中,Javac 編譯器完成了程序代碼通過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的字節碼指令流的過程。由於這一部分動做是在 Java 虛擬機以外進行的,而解釋器在虛擬機內部,因此 Java 程序的編譯就是半獨立的實現。
Java 語言常常被定位爲「解釋執行」的語言,在 Java 初生的 JDK1.0 時代,這種定義還算準確,但當主流的虛擬機中包含了即時編譯器後,Class 文件中的代碼到底會被解釋執行仍是編譯執行,就成了只有虛擬機本身才能準確判斷的事情。
Java 編譯器輸出的指令流,基本上是一種基於棧的指令集架構,指令流中的指令大部分是零地址指令,它們依賴操做數棧進行工做。與之相對的另一套經常使用的指令集架構是基於寄存器的指令集,最典型的就是 x86 的二地址指令集,這些指令依賴寄存器進行工做。