Java虛擬機-虛擬機字節碼執行引擎

全部Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果;java

 
運行時棧幀結構
棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構,是虛擬機運行時數據區中虛擬機棧的棧元素。
棧幀中存儲了局部變量表、操做數棧、動態連接和方法返回地址等信息;
每一個方法從調用開始到執行完成的過程,都對應着一個棧幀在虛擬機棧裏面中從進棧到出棧的過程。
 
每個棧幀都包括了局部變量表、操做數棧、動態連接和方法返回地址和一些額外的附加信息。
在編譯代碼的時候,棧幀中須要多大的局部變量表,多深的操做數棧都已經徹底肯定了,而且寫入到了方法表的Code屬性中;
 
一個線程中的方法調用鏈可能會很長,不少方法都同時處於執行狀態。對於執行引擎來講,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀。
與這個棧幀相關聯的方法稱爲當前方法。
執行引擎運行的全部字節碼指令都只是針對當前棧幀進行操做;
局部變量表
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序編譯爲Class文件的時候,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的局部變量表的最大容量;
局部變量表是以Slot(容量槽)爲最小單位;一個Slot能夠存放一個32位之內的數據類型;
Java中佔用32位之內的數據類型有boolean、byte、char、short、int、float、reference和returnAddress8中數據類型;
Java語言中明確的64位數據類型只有long和double兩種。須要用兩個連續的Slot來存放。
因爲局部變量表創建在線程的堆棧上,是線程私有的數據,不管讀寫兩個連續的Slot是否爲原子操做,都不會引發數據安全問題。
 
虛擬機經過索引定位的方式使用局部變量表,索引值的範圍從0開始至局部變量表最大的Slot數量。
爲了儘量節省棧幀空間,局部變量表中的Slot是能夠重用的。
若是訪問的是32位數據類型的變量,索引n就是表明了使用第n個Slot,若是訪問的是64位數據類型變量,則說明會同時使用n和n+1兩個Slot;
 
類變量有兩次賦初始值的過程,一次是在準備階段,賦予系統初始值;另一次是在初始化階段,賦予程序員定義的初始值;即便在初始化階段程序員沒有爲類變量賦值也沒有關係,類變量仍然具備一個肯定的初始值。
 
操做數棧
操做數棧也常稱爲操做棧,它是一個後入先出棧。
操做數棧的最大深度在編譯的時候就寫入方法表的Code屬性的max_statcks數據項中;
操做數棧的每個元素能夠是任意的Java數據類型,包括long和double,32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。
當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做。
Java虛擬機的解釋執行引擎稱爲「基於棧的執行引擎」,其中所指的「棧」就是操做數棧。
 
動態鏈接
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。
 
方法返回地址
當一個方法開始執行後,只有兩種方式能夠退出這個方法:
一、執行引擎遇到任意個方法返回的字節碼指令,這種退出方法的方式稱爲正常完成出口。
二、在方法執行過程當中遇到異常,而且這個異常沒有在方法體內獲得處理,這種退出方法的方式稱爲異常完成出口。
 
方法調用
方法調用並不等同於方法執行,方法調用階段惟一的任務就是肯定被調用方法的版本,暫時不涉及方法內部的具體運行過程。
 
解析
類加載的解析階段,會將其中的一部分符號引用轉化爲直接引用。
解析能成立的前提是:方法在程序真正運行以前就有一個能夠肯定的調用版本,而且這個方法的調用版本在運行期是不可改變的。
調用目標在程序代碼寫好、編譯器進行編譯時就必須肯定下來。
在Java語言中符合「編譯器可知,運行期不可變」這個要求的方法,主要包括靜態方法和私有方法兩大類;前者與類型直接關聯,後者在外部不可被訪問,這兩種方法各自的特色決定了他們都不可能經過繼承或別的方式重寫其餘版本,所以它們都適合在類加載階段進行解析;
在Java虛擬機裏面提供了5條方法調用字節碼指令,以下:
一、invokestatic:調用靜態方法;
二、invokespecial:調用實例構造器<init>方法、私有方法和父類方法;
三、invokevirtual:調用全部虛方法;
四、invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象
五、invokedynamic:如今運行時動態解析出調用點限定符所引用的方法,而後再執行該方法,在此以前的4條調用指令,分派邏輯是固化在Java虛擬機內部的,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
只要能被invokestatic和invokespecial指令調用的方法,均可以在解析階段中肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實力構造器、父類方法4類,它們在類加載的時候就會把符號引用解析爲該方法的直接引用。這些方法稱爲非虛方法。
 
分派
靜態分派,全部依賴靜態類型來定位方法執行版本的分派動做;發生在編譯階段,所以肯定靜態分派的動做實際上不是由虛擬機來執行的。
例如重載,虛擬機在重載的時候是經過參數的靜態類型而不是實例類型做爲判斷依據的,而且靜態類型是編譯器可知的。
動態分派,在運行期根據實際類型肯定方法執行版本的分派工做;例如重寫
單分派與多分派
方法的接收者與方法的參數統稱爲方法的宗量,根據分派基於多少種宗量,能夠將分派劃分爲單分派和多分派;
單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多於一個宗量對目標方法進行執行;
 
動態類型語言支持
動態類型語言
靜態類型語言:在編譯期肯定類型,最顯著的好處是編譯器能夠提供嚴謹的類型檢查,這樣與類型相關的問題能在編碼的時候及時發現,利於穩定性及代碼達到更大規模;
動態類型語言:在運行期肯定類型,這能夠爲開發人員提供更大的靈活性,某些在靜態類型語言中須要大量臃腫代碼來實現的功能,由動態類型語言來實現能夠更加清晰和簡潔,清晰和簡潔一般也就意味着開發效率的提高。
 
JDK1.7與動態類型
目前已經有許多動態類型語言運行於Java虛擬機之上了,如Clojure、Groovy、Jython和JRuby等。
方法符號引用在編譯時產生,而動態類型語言只有在運行期才能肯定接收者類型。這樣,在Java虛擬機上實現的動態類型語言就不得不使用其餘方式實現,這樣勢必讓動態類型語言實現的複雜度增長,也可能帶來額外的性能或者內存開銷。
 
java.lang.invoke包
主要是在以前單純依靠符號引用來肯定調用的目標方法這種方式之外,提供一種新的動態肯定目標方法的機制,稱爲MethodHandle。
僅站在Java語言的角度來看,MethodHandle的使用方法和效果與Reflection有衆多類似之處,不過它們還有如下區別:
一、從本質上將,Reflection和MethodHandle機制都是在模擬方法調用,但Reflection是在模擬Java代碼層次的方法調用,MethodHandle是在模擬字節碼層次的方法調用。
二、Reflection中的java.lang.reflect.Method對象遠比MethodHandle機制中的java.lang.invoke.MethosHandle對象所包含的信息多。
前者是方法在Java一端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各類屬性的java端表現方式,還包含執行權限等的運行期信息。
後者僅僅包含與執行該方法有關的信息。
Reflection是重量級,MethodHandle是輕量級
三、MethodHandle是對字節碼的方法指令調用的模擬,因此理論上虛擬機在這方面作的各類優化
 
Reflection API的設計目標只是爲了Java語言服務的
MethodHandle 則設計成可服務於全部虛擬機之上的語言,其中也包含java語言。
 
invokedynamic指令
在某種程度上,invokedynamic指令與MethodHandle機制的做用是同樣的,都是爲了解決原有4條"invoke*"指令方法分派規則固化在虛擬機之中的問題,把如何查找目標方法的決定權從虛擬機轉嫁到具體用戶代碼中,讓用戶有更高的自由度。
 
基於棧的字節碼解釋執行引擎
     因爲目前的主流虛擬機都包含了即時編譯器,因此Java已經再也不是純解釋執行的語言,並且Java編譯器輸出的指令集是基於棧的。
     Java編譯器輸出的指令流,基本上是一種基於棧的指令集架構,指令流中的指令大部分都是零地址指令,它們依賴操做數棧進行工做。
     基於棧的指令集主要的優勢是可移植,寄存器由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免的要受到硬件的約束。
     棧架構指令集的主要缺點是執行速度相對來講會慢一些。
相關文章
相關標籤/搜索