Java的技術(shù)體系包括
- 支持Java程序運行的虛擬機(JVM)
- 提供接口支持的Java API
- Java 編程語言
- 第三方Java框架(如Spring等)
代碼編譯的結(jié)果從本地機器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步,確實編程語言的一大步。
執(zhí)行引擎
執(zhí)行引擎是java虛擬機執(zhí)行字節(jié)碼指令的發(fā)動機和核心部件。虛擬機是一種相對于物理機的概念,兩種機器都有執(zhí)行代碼的能力。物理機的執(zhí)行引擎是建立在具體的處理器,硬件,指令集,操作系統(tǒng)之上的,而虛擬機執(zhí)行引擎是不依賴上述這些具體實現(xiàn),建立在自己的概念模型之上,因此可以自行指定指令集和引擎的結(jié)構(gòu)體系。
java虛擬機的執(zhí)行引擎從外部來看是一樣的:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程,輸出的是執(zhí)行結(jié)果。不過從內(nèi)部來講,各種不同的虛擬機又有自己的實現(xiàn)方式,比如最常見的有解釋執(zhí)行和便已執(zhí)行等。不過從虛擬機執(zhí)行引擎的概念模型角度來看,他們都是執(zhí)行的過程都是一樣的
棧幀
棧幀是虛擬機在進行方法調(diào)用和方法執(zhí)行過程中使用到的數(shù)據(jù)結(jié)構(gòu),它是虛擬機內(nèi)存中虛擬機棧的單位元素。棧幀存儲了方法的局部變量表,操作數(shù)棧,動態(tài)連接,和方法返回地址還有一些額外的信息。在代碼編譯過程中,一個方法需要用到的棧幀需要多少內(nèi)存空間就已經(jīng)完全確定了,并放在了Class文件中方法表的Code屬性中,因此棧幀的大小不會首運行時的影響。
每個方法從調(diào)用開始到完成的過程,都對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。實際一個線程運行過程中,方法調(diào)用鏈可能很長,所以一個典型的虛擬機棧中的棧幀結(jié)構(gòu)如下圖所示
下面詳細介紹一下棧幀中各個部分的作用以及數(shù)據(jù)結(jié)構(gòu)
局部變量表
局部變量表是用來存儲方法的參數(shù)和方法內(nèi)局部變量的存儲空間。java代碼在編譯為Class文件的時候,會在方法表的Code屬性的max_locals數(shù)據(jù)項中確定局部變量表的大小。它的容量以變量槽(Slot)為基本單位,Slot的大小通常為32位(虛擬機規(guī)范中的定義為,Slot應該能存放一個boolean, byte, char, short, int, float, refrence, returnAdress)。虛擬機通過索引的方式使用局部變量表,索引值從0開始到最大的Slot值。局部變量表的空間分配,0位索引默認存放該方法所屬對象的引用,也就是java語言關(guān)鍵字"this",然后按照參數(shù)表順序分配,之后是方法體內(nèi)部的局部變量。同時,虛擬機會根據(jù)Slot的作用域重用其空間。
對于refrence類型,指的是一個實例對象的引用,虛擬機應當能通過這個引用做到兩點,1.從此引用直接或間接的查找到對象在Java堆內(nèi)存中的起始地址,2.從此引用中直接或者間接地查找到對象所述數(shù)據(jù)類型在方法區(qū)中的類型信息。
操作數(shù)棧
操作數(shù)棧顧名思義是一個后入先出的棧,用來存放程序執(zhí)行過程中所有的操作數(shù)。它的最大深度也在編譯的時候就已經(jīng)寫入了Code屬性的max_stacks數(shù)據(jù)項中,操作數(shù)棧的每一個元素可以是任意的java數(shù)據(jù)類型,32為數(shù)據(jù)類型占用一個棧容量,64為數(shù)據(jù)類型占用兩個棧容量。
操作數(shù)棧的功能是在方法執(zhí)行的過程中,會有各種字節(jié)碼指令在操作數(shù)棧中寫入和提取內(nèi)容。比如算數(shù)運算中的操作數(shù)和結(jié)果,方法調(diào)用時候參數(shù)的傳遞等。
所以Java虛擬機的執(zhí)行引擎被稱之為“基于棧的執(zhí)行引擎”,就是指的操作數(shù)棧,也就是說所有的運算過程都是圍繞棧進行的。
動態(tài)連接
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,目的是為了支持方法調(diào)用過程中的動態(tài)連接。常量池中的符號引用通常情況下會在類加載階段或者第一次使用的時候,被轉(zhuǎn)化為直接引用,這種稱為靜態(tài)連接,有時候符號引用會在每一次使用的時候轉(zhuǎn)化為直接引用,這種被稱之為動態(tài)鏈接。
方法返回地址
當退出一個方法執(zhí)行的時候,需要返回到被調(diào)用的位置。因此方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的調(diào)用者的執(zhí)行狀態(tài)。比如,方法正常退出時,調(diào)用者的PC計數(shù)器的值可以作為返回地址,棧幀中很可能會保存這個地址。
一個方法退出時,執(zhí)行的操作可能會有:
- 恢復上層方法的局部變量表和操作數(shù)棧,
- 把返回值壓入調(diào)用棧幀的操作數(shù)棧中,
- 調(diào)整PC計數(shù)器的值指向繼續(xù)執(zhí)行的下一條指令
附加信息
虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀當中,這部分稱之為附加信息。
一般我們把動態(tài)連接、方法返回地址、其他附加信息歸為一類,稱之為棧幀信息
方法調(diào)用
方法調(diào)用是在調(diào)用方法的時候,確定調(diào)用的是哪個版本的方法。由于java語言在編譯過程中不包含連接過程,也就是說Class文件中存儲的是符號引用,而不是實際運行過程中的入口地址,這就導致Java的方法調(diào)用過程更為復雜,需要在類加載甚至運行期間才能確定方法的入口地址。方法的調(diào)用可以分為一下三種情況:解析、分派、動態(tài)類型語言支持。
解析
在類加載階段,會有一部分的符號引用轉(zhuǎn)化為直接引用,這樣的解析能成立的條件是,方法在程序運行之前就有一個可確定的調(diào)用版本,并且這個版本在運行過程中是不可改變的。這也就是類加載的解析階段。
java語言規(guī)范中,符合這樣“編譯器可知,運行期間不可變”的要求的方法,主要是靜態(tài)方法和私有方法兩大類。與java語言規(guī)范相對應的,在java虛擬機中提供了5條方法調(diào)用字節(jié)碼指令。分別為
- invokestatic 調(diào)用靜態(tài)方法
- invokespecial 調(diào)用實例構(gòu)造器,私有方法和父類方法
- invokevirtual 調(diào)用所有虛方法
- invokeinterface 調(diào)用接口方法
- invokedynamic
能被前兩條字節(jié)碼指令也就是 invokestatic invokespecial 調(diào)用的方法,是可以在解析階段就確定唯一版本的,符合這個條件的包括靜態(tài)方法、私有方法、構(gòu)造器、父類方法這四類,因此也被稱之為非虛方法。其他方法則被稱之為虛方法。
final方法,雖然在字節(jié)碼指令層面是通過invokevirtual調(diào)用,但是java語言規(guī)范明確規(guī)定,final方法是一種非虛方法。
分派
分派對應的java語言的“重寫”和“重載”。分派調(diào)用可能是靜態(tài)的,也可能是動態(tài)的。下面來看一下虛擬機中的方法分派具體是如何進行的
靜態(tài)分派
我們看下面一段代碼
Human man = new Man();
在這段代碼中,Human 稱之為變量的靜態(tài)類型,Man 稱之為變量的實際類型。靜態(tài)類型是在編譯期間就可以知道的,并且變量的靜態(tài)類型是不會發(fā)生變化的,而實際類型是要到運行期間才可以知道。編譯器在面對重載的方法時,是通過參數(shù)的靜態(tài)類型作為判別的依據(jù),因此,在編譯期間,編譯器便會根據(jù)靜態(tài)類型決定使用方法的哪個重載版本,并把該方法的符號引用寫到調(diào)用的字節(jié)碼指令 invokevirtual指令的參數(shù)中。
所以,依賴靜態(tài)類型來定位方法執(zhí)行版本的動作稱之為靜態(tài)分派,最典型的就是方法的重載。靜態(tài)分派發(fā)生在編譯階段,因此靜態(tài)分派并不是由虛擬機來完成的。
動態(tài)分派
動態(tài)分派對應著多態(tài)性的另外一個重要的體現(xiàn),就是重寫。調(diào)用重寫的方法對應著 invokevirtual 的字節(jié)碼指令,該指令的運行解析過程大致如下
- 找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記為C
- 如果在C中找到需要調(diào)用的方法,并且權(quán)限允許訪問,則返回該方法
- 否則,按照繼承關(guān)系從下往上一次對C的父類進行第2步的搜索和驗證
- 如果還是沒有找到,則拋出 java.lang.AbstraceMethodError 異常
我們把上述這種運行期間根據(jù)實際類型確定方法執(zhí)行版本的過程,稱之為動態(tài)分派
單分派和多分派
方法的接受者和方法的參數(shù)統(tǒng)稱為,方法的宗量。單宗量是根據(jù)一個方面對目標方法進行選擇,多分派則是根據(jù)多于一個宗量對目標方法進行選擇。比如,一個方法版本的選擇,既要依賴參數(shù)的靜態(tài)類型,同時又要調(diào)用者的實際類型,則就是多分派的方式。
基于棧的執(zhí)行引擎
在知道了方法時如何調(diào)用的問題之后,就需要解決方法是如何執(zhí)行的問題了。java虛擬機在執(zhí)行的時候通常有 解釋執(zhí)行 和 編譯執(zhí)行 兩種方式。java語言代碼經(jīng)過編譯器編譯后形成字節(jié)碼指令流,解釋器位于虛擬機的內(nèi)部,將進入虛擬機的字節(jié)碼指令流解釋執(zhí)行。
java編譯器輸出的指令流,是一種基于棧的指令集架構(gòu),指令集中的大部分指令都是零地址指令,它們依賴于操作數(shù)棧進行工作。與之相對應的另一套指令集架構(gòu)就是基于寄存器的指令集架構(gòu),也就是我們PC機中使用的指令集架構(gòu)。
基于棧的指令集
- 優(yōu)點,可移植性好不依賴于具體硬件,代碼更加緊湊,編譯器實現(xiàn)更加簡單
- 缺點,由于基于棧的操作,就導致出棧入棧的很多操作都需要進行,增加了指令的數(shù)量,同時操作數(shù)棧位于內(nèi)存中,相比寄存機位于處理器而言,速度會更慢。因此主要的缺點就是執(zhí)行會慢一些
基于寄存器的指令集
- 有點就是速度快
- 缺點就是可移植性差
總結(jié)
通過以上六篇文章,我們分別闡述了Java程序是如何存儲的(Class文件),如何被載入虛擬機的(類加載過程),以及如何執(zhí)行的(基于棧的字節(jié)碼執(zhí)行過程)。這就是虛擬機執(zhí)行系統(tǒng)最核心的三個問題。