JVM總結(五):JVM字節碼執行引擎


JVM字節碼執行引擎
  運行時棧幀結構
    局部變量表
    操作數棧
    動態連接
    方法返回地址
    附加信息
  方法調用
    解析
    分派 –“重載”和“重寫”的實現
      靜態分派
      動態分派
      單分派和多分派
      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之類的默認值。這一點要好好注意一下。

操作數棧

  操作棧,它是一個后入先出棧。同局部變量表一樣,操作數棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中。 
操作數棧的每一個元素可以是任意的Java數據類型,包括long和double。32位數據類型所占的棧容量為1,64位所占的棧容量為2.在方法執行的任何時候,操作數棧的深度都不會超過在max_stacks數據項中設定的最大值。

  當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令向操作數棧中寫入和提取內容,也就是入棧出棧操作。 
操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。 
  另外,在概念模型中,兩個棧幀作為虛擬機棧的元素,相互之間是完全獨立的。但是大多數的虛擬機的實現里都會做一些優化處理,令兩個棧幀出現一部分重疊。這樣在進行方法調用時就可以共用一部分數據,而無須進行額外的參數復制傳遞。

  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指令的參數中。 
  所有依賴於靜態類型來定位方法執行版本的分派動作稱為靜態分派,靜態分派的典型是方法的重載。 
  靜態分派發生的時間:靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。 
  靜態方法會在類加載期進行解析,而靜態方法顯然是可以擁有重載版本的,選擇重載版本的過程也是通過靜態分派完成的。

動態分派

  運行期根據實際類型確定方法執行版本的分派過程稱為動態分派。動態分派與方法重寫有着密切的關系。

單分派和多分派

  方法的接收者與方法的參數統稱為方法的宗量。而根據分派基於多少種宗量,可以將分派划分為單分派和多分派兩種,單分派是根據一個宗量對目標方法進行選擇,多分派是根據多於一個宗量對目標方法進行選擇。

JVM動態分派的實現

  由於動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要運行時在類的方法元數據中搜索合適的目標方法,為了避免頻繁的搜索,最常用的“穩定優化”的手段就是為類在方法區中建立一個虛方法表,使用虛方法表索引來代替元數據查找以提高性能。 
  虛方法表中存放着各個方法的實際入口地址,如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都是指向父類的實現入口,如果子類中重寫了這個方法,子類方法表中的地址將會替換成指向子類實現版本的入口地址。 
  為了程序實現上的方便,具有相同的簽名的方法,在父類、子類的虛方法表中都應當具有一樣的索引序號,這樣當類型變換時,僅需要變更在、查找的方法表,就可以從不同的虛方法表中按照索引轉換出所需的入口地址。

基於棧的字節碼解釋執行引擎

 
  Java程序在執行前先對程序源碼進行詞法分析和語法分析處理,把源碼轉化為抽象語法樹。對於一門具體語言的實現來說,詞法分析、語法分析以及后面的優化器和目標代碼生成器都可以選擇獨立於執行引擎,形成一個完整意義的編譯器去實現,這類代表是C/C++語言。當然也可以選擇其中的一部分步驟實現一個半獨立的編譯器,這類代表是Java語言。又或者把這些步驟和執行引擎全部集中封裝到一個封閉黑匣子中,如大多數的JS執行器。

基於棧的指令集與基於寄存器的指令集

  Java編譯器輸出的指令流,基本上是一種基於棧指令集架構,指令流中的指令大部分都是零地址指令,它們依賴操作數棧進行工作。 
  基於棧的指令集主要優點就是可移植。除此之外,還有其他的優點,如代碼相對更加緊湊(字節碼中每個字節就對應一條指令,而多地址指令集中還需要存放參數)、編譯器實現更加簡單等。 
  缺點是:執行速度相對較慢。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM