JVM字節碼執行引擎
運行時棧幀結構
局部變量表
操做數棧
動態鏈接
方法返回地址
附加信息
方法調用
解析
分派 –「重載」和「重寫」的實現
靜態分派
動態分派
單分派和多分派
JVM動態分派的實現
基於棧的字節碼解釋執行引擎
基於棧的指令集與基於寄存器的指令集程序員
虛擬機是相對於「物理機」而言的,這兩種機器都有代碼執行能力,其區別主要是物理機的執行引擎是直接創建在處理器、硬件、指令集和操做系統層面上的,而虛擬機的執行引擎是本身實現的。所以程序員能夠自行制定指令集和執行引擎的結構體系,而且可以執行那些不被硬件直接支持的指令集格式。
在Java虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型,這個概念模型稱爲各類虛擬機執行引擎的統一外觀。虛擬機實現中,可能會有兩種的執行方式:解釋執行(經過解釋器執行)和編譯執行(經過即時編譯器產生本地代碼)。有些虛擬機值採用一種執行方式,可是有點採用了兩種,甚至有可能包含幾個不一樣級別的編譯器執行引擎。
全部的Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件、處理過程是等效字節碼解析過程,輸出的是執行結果。安全
棧幀(Stack Frame)是一種數據結構,它主要是用來支持虛擬機進行方法調用和方法執行。它是虛擬機運行時數據區的虛擬機棧的棧元素。
包含內容:棧幀包含了局部變量表、操做數棧、動態鏈接、方法返回地址和一些額外的附加信息等。
執行過程:一個線程中的方法調用鏈可能會很長,不少方法都同時處於執行狀態。在活動線程中,只有棧頂的棧幀纔是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法,執行引擎所運行的全部的字節碼指令都只針對當前棧幀進行操做。
執行意義:每一個方法從調用開始到執行完成的過程,就對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。數據結構
值得注意的是:在編譯程序代碼的時候,棧幀須要多大的局部變量表、多深的操做數棧都已經徹底肯定了,而且寫入到方法表的Code屬性之中,所以一個棧幀須要分配多大的內存,並不會受到運行期變量數據的影響,而僅僅取決於具體的虛擬機的實現。架構
一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中肯定了改方法所需分配的最大局部變量表的容器。
包含類型:boolean、byte、char、short、int、float、reference或returnAddress類型八種類型。
容量單位:變量槽(slot)。不過虛擬機中並無明確肯定每個變量槽所佔據的內存空間大小,只是有導向性地說明每一個變量槽都應該存放的八種類型:boolean、byte、char、short、int、float、reference或returnAddress類型的數據。這種描述和明確指出「每一個Slot佔用32位長度的內存空間」有一些差異,它容許Slot的長度隨着不一樣的處理器、操做系統或者虛擬機而發生改變。在64位系統上使用64位長度的內存空間來實現一個slot,虛擬機仍要使用對齊和補白的手段讓Slot在外觀上看起來與32位虛擬機中的一致。佈局
在Java中佔32位之內的數據類型有boolean、byte、char、short、int、float、reference或returnAddress類型等,前六種不解釋,然後面的reference是對象的引用。虛擬機規範並無說明它的長度,也沒有明確指出這個引用應有怎樣的結構,但通常來講:虛擬機實現至少都應當能今後引用中直接或間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。而returnAddress是爲字節碼指令jsr、jsr_w和ret服務的,它指向一條字節碼指令的地址。
對於64爲的數據類型,虛擬機會以高位在前的方式爲其分配兩個連續的Slot空間。即long和double兩種類型。作法是將long和double類型速寫分割爲32位讀寫的作法。不過因爲局部變量表創建在線程的堆棧上,是線程的私有數據,不管讀寫兩個連續的Slot是不是原子操做,都不會引發數據安全問題。性能
虛擬機索引方式:虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從0開始到局部變量表最大的Slot數量。若是是32爲數據類型的數據,索引n就表示使用第n個Slot,若是是64位數據類型的變量,則說明要使用第n和第n+1兩個Slot。
在方法執行過程當中,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程。若是是實例方法(非static方法),那麼局部變量表中的第0位索引的Slot默認是用來傳遞方法所屬對象實例的引用,在方法中能夠經過關鍵字「this」來訪問這個隱含的參數。其他參數按照參數表的順序來排列,佔用從1開始的局部變量Slot,參數表分配完畢後,再根據方法體內部定義的變量順序和做用域分配其他的Slot。
局部變量表中的Slot是可重用的,方法體中定義的變量,其做用域並不必定會覆蓋整個方法體,若是當前字節碼PC計數器的值已經超過了某個變量的做用域,那麼這個變量相應的Slot就能夠交給其餘變量去使用。節省棧空間。但也有可能會影響到系統的垃圾收集行爲。優化
還有一點要說明的是:局部變量不像前面介紹的類變量那樣存在「準備階段」。咱們知道,類變量在加載過程當中要通過兩次賦初始值的過程:一次在準備階段,賦予系統初始值,另一次在初始化階段,賦予程序員定義的初始值。但局部變量不同,若是一個局部變量定義了可是沒有賦初始值是不能使用的。全部不要認爲Java中任何狀況下都存在着諸如整型變量默認爲0,布爾型變量默認爲false之類的默認值。這一點要好好注意一下。this
操做棧,它是一個後入先出棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中。
操做數棧的每個元素能夠是任意的Java數據類型,包括long和double。32位數據類型所佔的棧容量爲1,64位所佔的棧容量爲2.在方法執行的任什麼時候候,操做數棧的深度都不會超過在max_stacks數據項中設定的最大值。spa
當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令向操做數棧中寫入和提取內容,也就是入棧出棧操做。
操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。
另外,在概念模型中,兩個棧幀做爲虛擬機棧的元素,相互之間是徹底獨立的。可是大多數的虛擬機的實現裏都會作一些優化處理,令兩個棧幀出現一部分重疊。這樣在進行方法調用時就能夠共用一部分數據,而無須進行額外的參數複製傳遞。操作系統
Java虛擬機的解釋執行引擎稱爲「基於棧的執行引擎」,其中的棧就是指操做數棧。
每一個棧幀都包含着一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用的是爲了支持方法調用過程當中的動態鏈接。
在Class文件中存在着大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分在類加載階段第一次使用階段的時候轉換爲直接引用,這種轉換稱爲靜態解析。另一部分將在每次的運行期間轉化爲直接引用,這部分稱爲動態轉換。
當一個方法被執行後,有兩種方式能夠退出這個方法。
第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口。
另一種退出方式是:在方法執行過程當中遇到異常,而且這個異常沒有在方法體內獲得處理,不管是JVM內部產生的異常,仍是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出。這種方式被稱爲異常退出出口。此方式不會給上層調用者產生任何返回值。
不管採用哪種退出方式,在方法退出後,都會返回到方法被調用的位置,程序才能繼續執行。方法返回時可能要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出以後,調用者的PC計數器的值就能夠做爲返回地址。棧幀中極可能會保存這個計數器值,而方法異常退出後,返回地址就要經過異常處理器表來肯定,棧幀通常不保存這部分信息。
方法退出實際上就是把當前棧幀出棧的操做:所以退出時可能執行的操做:恢復上層方法局部變量表和操做數棧,把返回值壓入調用者棧幀的操做數棧中,調整PC計數器的值以指向指令後面的一條指令。
增長一些沒有描述的信息到棧幀之中。通常將動態鏈接、方法返回地址和其餘附加信息所有歸爲一類,稱爲棧幀信息。
Class文件的編譯過程當中不包含傳統編譯中的鏈接步驟,一切方法調用都在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存佈局中的入口地址(至關於以前所說的直接引用)。這個特性給Java帶來了更強大的動態擴展能力,但也使得Java方法調用過程變得相對複雜起來,須要在類加載期間,甚至到運行期間才能肯定目標方法的直接引用。
全部的方法調用的目標方法在Class文件裏面都只是一個常量池的符號引用,在類加載的解析階段,會將其中的一部分符號引用轉化爲直接引用,這種解析能成立的前提是L方法在程序真正運行以前就有一個可肯定的調用版本,而且這個方法的調用版本在運行期是不會改變的。換句話說:調用目標在程序代碼寫好、編譯器進行編譯時就必須肯定下來,這類方法的調用稱爲解析。
在JVM中提供了5條方法調用字節碼指令,分別是:
invokestatic:調用靜態方法
involespecial:調用實例構造器方法、私有方法和父類方法。
invokevirtual:調用全部的虛方法。
invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象。
invokedynamic:先在運行時動態解析出調用限定符所引用的方法,而後再執行該方法。
只要能被invokestatic和invokeapecial指令調用的方法,都是能夠在解析階段肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器、父類方法4類,他們在類加載的時候就會把符號引用解析爲該方法的直接引用,這些方法能夠稱爲非虛方法,與之相反,其餘方法稱爲虛方法(除去final方法)。非虛方法除了上述的兩種之外,還有一種就是被final修飾的方法,雖然final方法是使用invokevirtual指令來調用的,可是因爲它沒法被覆蓋,因此能夠把final方法看做是一種非虛方法。
解析調用必定是個靜態的過程,在編譯期間就能夠肯定,在類裝載的解析階段就會把涉及的符號引用所有轉變爲可肯定的直接引用,不會延遲到運行期再去完成。而分派調用則多是靜態的或者是動態的,根據分派依據的總量數能夠分爲單分派和多分派,這兩種分派方式的兩兩組合就構成了靜態單分派、靜態多分派、動態單分派和動態多分派四種分派組合。
JVM在重載時是經過參數的靜態類型而不是實際類型作斷定的,而且靜態類型是編譯期可知的,所以在編譯階段,Javac編譯器會根據參數的靜態類型決定使用那個重載版本,而後再把該方法的符號引號寫到main()方法的兩條invokevritual指令的參數中。
全部依賴於靜態類型來定位方法執行版本的分派動做稱爲靜態分派,靜態分派的典型是方法的重載。
靜態分派發生的時間:靜態分派發生在編譯階段,所以肯定靜態分派的動做實際上不是由虛擬機來執行的。
靜態方法會在類加載期進行解析,而靜態方法顯然是能夠擁有重載版本的,選擇重載版本的過程也是經過靜態分派完成的。
運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派。動態分派與方法重寫有着密切的關係。
方法的接收者與方法的參數統稱爲方法的宗量。而根據分派基於多少種宗量,能夠將分派劃分爲單分派和多分派兩種,單分派是根據一個宗量對目標方法進行選擇,多分派是根據多於一個宗量對目標方法進行選擇。
因爲動態分派是很是頻繁的動做,並且動態分派的方法版本選擇過程須要運行時在類的方法元數據中搜索合適的目標方法,爲了不頻繁的搜索,最經常使用的「穩定優化」的手段就是爲類在方法區中創建一個虛方法表,使用虛方法表索引來代替元數據查找以提升性能。
虛方法表中存放着各個方法的實際入口地址,若是某個方法在子類中沒有被重寫,那子類的虛方法表裏面的地址入口和父類相同方法的地址入口是一致的,都是指向父類的實現入口,若是子類中重寫了這個方法,子類方法表中的地址將會替換成指向子類實現版本的入口地址。
爲了程序實現上的方便,具備相同的簽名的方法,在父類、子類的虛方法表中都應當具備同樣的索引序號,這樣當類型變換時,僅須要變動在、查找的方法表,就能夠從不一樣的虛方法表中按照索引轉換出所需的入口地址。
Java程序在執行前先對程序源碼進行詞法分析和語法分析處理,把源碼轉化爲抽象語法樹。對於一門具體語言的實現來講,詞法分析、語法分析以及後面的優化器和目標代碼生成器均可以選擇獨立於執行引擎,造成一個完整意義的編譯器去實現,這類表明是C/C++語言。固然也能夠選擇其中的一部分步驟實現一個半獨立的編譯器,這類表明是Java語言。又或者把這些步驟和執行引擎所有集中封裝到一個封閉黑匣子中,如大多數的JS執行器。
Java編譯器輸出的指令流,基本上是一種基於棧指令集架構,指令流中的指令大部分都是零地址指令,它們依賴操做數棧進行工做。
基於棧的指令集主要優勢就是可移植。除此以外,還有其餘的優勢,如代碼相對更加緊湊(字節碼中每一個字節就對應一條指令,而多地址指令集中還須要存放參數)、編譯器實現更加簡單等。 缺點是:執行速度相對較慢。