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

概述

Java虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型,成爲各種虛擬機執行引擎的統一外觀(Facade)。不同的虛擬機引擎會包含兩種執行模式,解釋執行和編譯執行。

運行時幀棧結構

棧幀(Stack Frame)支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量、操作數棧、動態連接和方法返回地址等信息。方法調用開始到執行完成,對應這一個幀棧在虛擬機棧裏面入棧和出棧的過程。
一個線程中的方法調用鏈可能會很長,很多方法同時處於執行狀態,但是對於執行引擎來說只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀(Current Stack Frame),與其關聯的方法稱之爲當前方法(Current Method)。

局部變量表

局部變量表(Local Variable Table)變量值存儲空間,用於存儲方法參數和方法內部定義的局部變量。容量以變量槽(Variable Slot)爲最小單位,一個Slot可以存放一個32位以內的數據類型,在64位虛擬機中一個Slot使用64位的物理內存,會使用對齊和補白的手段讓Slot在外觀上看起來和32位虛擬機一致。對於64位的數據類型,虛擬機會以高位對齊的方式爲其分配兩個連續的Slot空間,這裏把long和double數據類型分割存儲的做法與“long和double的非原子性協定”中把一次long和double數據類型讀寫分隔位兩次32位讀寫的做法有些類似。
局部變量表中的對象是否能夠被垃圾回收的根本原因取決於Slot是否還存有對象的引用。如果有一個方法,後端的代碼有很耗時的操作,而前端又定義佔用了大量的內存,對於實際不再使用的變量手動設置爲Null能夠使得其被垃圾回收,否則即使離開了作用域,但是局部變量表作爲GC Roots的一部分仍然保持着對它的關聯,會導致內存一直佔用無法釋放。

操作數棧

操作數棧(Operand Stack)也常稱爲操作棧,他是一個先入後出(Last In First Out, LIFO)棧。
在概念模型中,兩個棧幀作爲虛擬機的元素是完全相互獨立的。但是在大多虛擬機的實現裏面都會做一些優化處理,令兩個棧幀出現一部分重疊,讓下面棧幀的部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣進行方法調用時就可以共用一部分數據,無需進行額外的參數複製傳遞。
Java虛擬機的解釋執行引擎稱爲“基於棧的執行引擎”,其中的棧指的就是操作數棧。

動態連接

每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接(Dynamic Linking)。Class文件的常量池中存在大量的符號引用,字節碼中方法掉紅指令就以常量池中指向方法的符號引用作爲參數。這些符號引用一部分會在類加載階段或者第一次使用的使用就轉化爲直接引用,這種轉化稱之爲靜態解析。另外一部分會在每一次運行期間轉化爲直接引用,這部分稱之爲動態連接。

方法返回地址

一個方法開始執行後,有兩種退出方式。第一種是執行引擎遇到任意一個方法返回的字節碼指令,也就是正常的return,這種退出方法稱之爲正常完成出口(Normal Method Invocation Completion)。
另一種退出方式是在方法執行過程中遇到了異常,這個異常沒有在方法體內得到處理,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方式稱之爲(Abrupt Method Invocation Completion)。
無論哪種退出方式,退出之後都要返回到方法被調用的位置程序才能繼續執行,方法返回時需要棧幀中保存一些信息,用來幫助恢復它上層方法的執行狀態。一般來說方法正常退出調用者的PC計數器的值可以作爲返回地址,棧幀中可能會保存這個計數器值。異常退出時,返回地址是要通過異常處理表來確認,棧幀中一般不會保存這部分信息。
方法退出過程實際上等同於把當前棧幀出棧,因此退出時可能的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有)壓入調用者棧幀的操作數棧,調整PC計數器的值以指向調用方法指令後面的一條指令等。

方法調用

方法調用不等於方法執行,方法調用爲了確定被調用方法的版本(即調用哪個方法),暫時不涉及方法內部的具體運行過程。Class文件的編譯過程中存儲的符號引用,而不是實際運行時內存佈局中的入口地址(直接引用)。Java方法調用需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。

解析

類加載的解析階段,方法調用中的部分符號引用轉化爲直接引用,基於的前提是:方法在程序真正運行之前就有一個可確定的調用版本,並且運行期間不可改變。換句話說調用目標在代碼寫好,編輯器編譯時就必須確定下來。這類方法的調用稱之爲解析(Resolution)。
Java語言符合“編譯期可知,運行期不可變”這個要求的方法,主要包括靜態方法和私有方法兩大類,前者與類型關聯,後者在外部不可被訪問,兩種方法各自的特點決定了他們都不可能通過繼承或別的方式重寫其他版本,他們都適合在類加載階段進行解析。
與之對應的,Java虛擬機提供了5條方法調用字節碼指令:
invokestatic:調用靜態方法。
invokespecial:調用實例構造器的 方法、私有方法和父類方法。
invokevirtual:調用所有的虛方法
invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。
invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,然後再執行該方法,再此之前的4條調用指令,分派邏輯時固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段確定唯一的調用版本,在類加載的時候會把符號引用解析爲該方法的直接引用,這些方法可以稱爲非虛方法,其他方法稱爲虛方法。

分派

分派實際上解釋瞭如”重載“和”重寫“在Java虛擬機之中是如何實現的。

  1. 靜態分派
    例如存在3個類:
    static abstract class Human
    static class Man extends Human
    static class Woman extends Human
    其中 Human 稱爲變量的靜態類型(Static Type) 或者外觀類型(Apparent Type),後面的Man稱爲變量的實際類型(Actual Type),編譯階段,Javac編譯期根據參數的靜態類型決定使用哪個重載版本,並且將對應方法的符號引用寫入invokevirtual指令的參數中。
    所有依賴靜態類型來定位方法執行版本的分派動作稱爲靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段,所以靜態分派的動作不是由虛擬機執行的。另外編譯器雖然能確定出方法的重載版本,但是很多情況下這個重載版本並不是“唯一的”,往往只能確定一個“更加合適的”版本,如 char 類型變量 'a' 可以被理解爲char,也可以理解爲int、long、Object、char...等等,所以在編譯時會“選擇一個更加合適”的意思就在於此。
  2. 動態分派
    invokevirtual指令的運行時解析過程大致分爲一下幾個步驟:
    1)找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C
    2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。
    3)否則,按照繼承關係從下往上一次對C的各個父類進行第二步的搜索和驗證過程。
    4)如果始終沒有找到合適方法,就拋出java.lang,AbstractMethodError異常。
    由於invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,這個過程實際上就是Java語言中方法重寫的本質。我們把這種運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。
  3. 單分派與多分派
    方法的接收者與方法的參數統稱爲方法的宗量,這個定義最早應該來源於《Java與模式》一書。根據分派基於多少種宗量,可以將分派劃分爲單分派與多分派。單分派是根據一個宗量對目標方法進行選擇,多分派則是根據多於一個宗量對目標方法進行選擇。
    Java1.7及之前,還是一門靜態多分派,動態多分派的語言。
  4. 虛擬機動態分派的實現
    前面的內容說明了虛擬機在分派的時候“會做什麼”,但是虛擬機“具體是如何做到的”,不同虛擬機實現都有些差異。
    由於動態分派是非常頻繁的行爲,而且動態分派的方法版本選擇過程需要運行時再類的方法元數據中搜索合適的目標方法,因此基於性能的考慮,大部分虛擬機實現都不會真正進行如此頻繁的搜索,最常見的穩定優化手段就是爲類在方法區中建立一個虛方法表(Virtual Method Table,稱爲vtable,對應的invokeinterface執行時用到接口方法表--Interface Method Table,稱爲itable)。
    除了方法表之外,在條件允許的情況下,還會使用內聯緩存(Inline Cache)和基於“類型繼承關係分析”(Class Hierarchy Analysis,CHA)技術的守護內聯(Guarded Inlining)兩種非穩定的激進優化的手段來獲得更高性能。

動態類型語言支持

Java虛擬機從第一款虛擬機到JDK7之前十餘年時間裏,都沒有改變過字節碼指令集,在JDK7中添加類invokedynamic指令,這是爲了實現“動態語言類型”(Dynamic Typed Language)支持進行的改進之一,也是爲了JDK8可以順利實現Lambda表達式做技術準備。

  1. 動態類型語言
    動態類型語言的關鍵特徵是它的類型檢查的主題過程是在運行期而不是編譯期,例如:Groovy,JavaScript,PHP,Lisp等,相對的在編譯期就進行類型檢查過程的語言(如C++ 和Java等)就是靜態類型語言。
    “變量無類型而變量值纔有類型”這個特點也是動態類型語言的一個重要特徵。
  2. java.lang.invoke包
    JDK1.7實現了JSR-292,加入了java.lang,invoke包,這個包的主要目的是在之前單純依靠符號引用來確定調用的目標方法之外,提供一種新的動態確定目標方法的機制稱之爲MethodHandle。
    Reflection和MethodHandle都是在模擬方法調用,但是Reflection是在模擬Java代碼層次的方法調用,而MethodHandle是在模擬字節碼層次的方法調用,MethodHandles.lookup的3個方法--findStatic(),findVirtual(),findSpecial()正是爲了對應invodestatic invokevirtual&invokeinterface和invokespecial這幾條字節碼指令的執行權限校驗行爲,這些底層細節在使用ReflectionAPI時是不需要關心的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章