深入了解jvm-2Edition-虛擬機字節碼執行引擎


1、概述

  Java虛擬機規范制定了虛擬機字節碼執行引擎的概念模型,本章主要從概念模型層次來探究虛擬機的方法調用和字節碼執行。

  方法調用中,最核心的,是如何確定調用的方法,也就是方法的分派

  字節碼執行過程中,特別重要的一點是執行上下文的切換和信息的交換處理。這需要運行時數據結構的支持,也就是運行時棧幀

2、運行時棧幀結構

  運行時棧幀(Stack Frame)是用於支持虛擬機方法調用和方法執行的數據結構。

  它是虛擬機運行時數據區中的虛擬機棧的棧元素。

  存儲了方法的局部變量表、操作數棧、動態鏈接和方法返回地址等信息。

  方法的調用、執行、返回過程就是棧幀在棧里入棧(創建)、內部信息改變、出棧(銷毀)的過程。

  在編譯過程中,棧幀中的局部變量表的大小、操作數棧的深度就已經確定並記錄在了方法的code屬性里面了。

  對於執行引擎來說,只有棧頂的棧幀(當前棧幀,對應當前方法)是有效的。

  1、局部變量表

    存放方法參數和方法內部定義的局部變量

    容量以槽(Slot)為最小單位。

    虛擬機規范沒有規定槽的大小,

    只說了每個槽都能存放一個boolean、byte、char、short、int、float、reference、returnAddress數據類型。

    因此可以說一個Slot可以存放一個32位及以下的數據類型。

    64位的數據類型要占用兩個Slot(long、double),高位對齊

    reference數據至少要能幫助虛擬機完成兩項功能:

      1、直接或間接地查找到對象在Java堆中的起始地址;

      2、直接或間接地在方法區中查找到對象所屬數據類型(對象的元數據)。

    局部變量列表中,索引從0開始,第0位存放的是方法隱含的參數this(非static方法)

    其余位置先按參數列表的順序存放參數,再按局部變量定義的順序存放局部變量。

    局部變量表中的引用會影響到GC的行為,因為它是GC Roots之一。

    如果局部變量表中的引用還存在,那么GC就不會清除引用指向的對象。

    將對象引用置為null來幫助GC的原理就是手動將局部變量表中對應的的Slot清空。

    置null操作意義不大,這通常會被編譯器優化掉。。。

    最重要的一點!局部變量表不像方法區中的類一樣有初始化賦值過程(准備階段)

    因此,沒有賦初始值的局部變量是不能使用的。不像類變量一樣有系統初始值。

  2、操作數棧

    操作數棧是方法執行的最基礎的支撐。

    操作數棧中元素的數據類型要與字節碼指令嚴格匹配,這在編譯時會保證,在類校驗階段還要再次驗證。

  3、動態鏈接

    指向方法區中運行時常量池中該棧幀所屬方法的引用,為了支持方法調用過程中的動態鏈接。

    靜態解析:在類加載或第一次使用的時候就將符號引用轉換為直接引用。

    動態鏈接:在運行期間才轉轉為直接引用。

  4、方法返回地址

    正常完成出口:方法正常執行退出

    異常完成出口:。。。

    方法退出過程就是將當前棧幀出棧,並恢復上層方法的局部變量表和操作數棧

    把返回值壓入上層方法的操作數棧中,調整PC的值,指向下一條指令。

  5、附加信息

    調試信息等。

3、方法調用

  方法調用不等同於執行,調用只是確定是哪一個方法(參數、返回值、所屬類)。

  1、解析

    調用目標在編譯期就確定,這就是解析調用。

    方法能解析的前提:方法在程序運行前就有一個可確定的調用版本,並且該版本在運行期不變。

    符合該前提的方法主要包括靜態方法和私有方法

    靜態方法直接和類關聯,私有方法不可訪問,因此它們都不可通過繼承或其他方式重寫。

    虛擬機中的方法調用指令:

      1、invokespecial調用構造器<init>,私有方法和父類方法。

      2、invokestatic調用靜態方法。

      3、invokevritual調用虛方法

      4、invokeinterface調用接口方法

      5、invokedynamic動態解析調用方法。

     只要能夠被1、2調用的方法都可以在解析時確定。

4、方法調用-分派

  解析調用在編譯期完成,是靜態的。

  分派則可以是靜態的也可以是動態的。

  按照宗量數又可分為單分派多分派。(方法接收者與參數統稱為方法宗量)

  因此,就可組合出:動/靜態單/多分派 四種分派方式。

  靜態分派是重載的虛擬機層面的實現。動態分派是重寫的虛擬機層面的實現。

    

  1、靜態分派

      Human man = new Man();

    其中,Human稱為變量的靜態類型(Apparent Type)Man稱為變更量的實際類型(Actual Type)

    靜態類型在編譯時就可以確定,但是實際類型要在運行時才能確定。

    其實,從英文名就很好理解,Apparent Type就是表面上的類型,Actual Type就是實際上的類型。

    對於man,在編譯時就可以確定它是一個Human類型,但是,他到底是Man還是Woman要等程序運行時才知道。

    方法被重載時,是通過靜態類型作為方法的選擇依據的,因此在編譯時就可以選定重載方法。

    依據靜態類型來定位方法的執行版本的分派就稱為靜態分派。

    所以,靜態分派不是虛擬機做的,它是編譯期做的。

  2、動態分派

      既然靜態分派是在編譯期,那么動態分派就在運行期咯。

      void sayHello(Human human){ human.hello(); }

      sayHello(man);

      sayHello(woman);

    對於上述代碼,怎么去確定human.hello()要調用的方法呢?

    

    javap 反編譯后,發現它們都是由invokevirtual調用的,但是,兩個invokevirtual都是指向的Humanhello()

    但是兩個執行的方法明顯是不同的。

    這就是因為invokevirtual指令的多態查找過程:

      1、找到操作數棧棧頂的元素指向的對象的實際類型,記為C。

        都找到實際類型了,多態不就解決了。

      2、在C中查找與invokevirtual指令參數常量描述符簡單名都相符的方法,

        找到后,要檢查訪問權限,權限不通過,則拋出IllegalAccessError異常。

      3、否則,到繼承鏈上尋找。

      4、否則,拋出AbstractMethodError異常。

    可以看出,invokevirtual指令的執行結果是和操作數棧的狀態相關的,

    還可以看出,調用對象方法時,首先要做的,就是將對象引用入棧。

    因此就多態就實現了。

  3、單分派和多分派

     

    方法的接收者與方法的參數統稱為方法的宗量。根據分派基於多少宗量,可以將分派划分為單分派和多分派。

    上面代碼中,對 father.Chioce(new Candy());處代碼 編譯期選擇依據兩點:

      注意father的類型是可編譯時確定的。因此為靜態分派

      1、靜態類型是Father還是Son

      2、方法參數是Candy還是Fist

      基於兩個宗量進行的,因此靜態分派屬於多分派類型

    對son.Choice(new Candy()); 處調用:

       son的類型在編譯期無法確定,因此為動態分派

       但是,此時編譯器已經指定了方法的參數必須是Candy類型的。

       因此,動態分派時只需要確定方法的所屬類。

       因此,Java的動態分派屬於單分派類型

    Java是靜態多分派,動態單分派的類型

  4、虛擬機動態分派實現

    出於性能考慮,在實現中,為類在方法區中建立了一個虛方法表(Virtual Method Table)

    用於invokevirtual指令執行時,直接在該虛方法表中查找方法。

    虛方法表中存放着各個方法的實際入口地址

    如果子類沒有重寫父類方法,那么子類的虛方法表中,該方法指向父類方法的實現入口。 

    如果子類重寫了,就指向子類自己的實現的入口。

    為了實現方便,相同簽名的方法在子類和父類虛方法表中的索引都一樣。

    虛方法表一般在類加載的鏈接階段初始化,就是在類第一次初始化之后。

    為了invokeinterface執行,也建立了接口方法表(Interface Method Table)

5、動態類型語言支持

  動態類型語言可以實現在運行時自由地為類綁定字段和方法,這就要求,在進行方法分派時,可以有自己的選擇。

  但是目前講到的分派,方法分派時的查找都是規定好了的。

  因此,要支持動態類型支持,就要將方法分派的接口分享出來,讓我們可以自己去進行分派。

  jdk1.7引入了java.lang.invoke包,提供了一種新的動態確定目標方法的機制:

  MethodHandle

    A method handle is a typed, directly executable reference to an underlying method, constructor, field,

    or similar low-level operation, with optional transformations of arguments or return values.

  也就是說,除了只能把類作為單獨實體來使用,我們可以通過MethodHandle將方法也抽象成一個單獨實體。

  (雖然也是通過類來實現的。。。)

  好了,我們現在能單獨使用方法了,但是,還得找到它吧。

  這就涉及到怎么確定一個方法

    1、方法所屬類

    2、方法簡單名

    3、方法描述符(參數,返回值)

  MethodType

    A method type represents the arguments and return type accepted and returned by a method handle,

    or the arguments and return type passed and expected by a method handle caller. 

  MethodType封裝了對方法描述符的表示。

  現在:

    1、類可以用類的Class對象表示;

    2、方法簡單名——字符串

    3、方法描述符——MethodType

  就可以去找方法了。

  MethodHandles類為我們提供了許多根據上述標識找方法的封裝。太貼心了。

  

  invokedynamic指令:

    同MethodHandle機制一樣,只是MethodHandle是上層實現,invokedynamic是底層實現

    每一處invokedynamic指令的位置都被稱作動態調用點(Dynamic Call Site)

    CallSite:

      A CallSite is a holder for a variable MethodHandle, which is called its target.

      An invokedynamic instruction linked to a CallSite delegates all calls to the site's current target.

    invokedynamic指令的第一個參數不是CONSTANT_Methodref_info常量,

    而是新增的CONSTANT_InvokeDynamic_info

    CONSTANT_InvokeDynamic_info包含三個信息:

      1、引導方法

      2、方法類型MethodType

      3、方法名稱

      根據前面分析,方法名稱、描述符有了,但是還差方法所屬類。所以,引導方法中,應該要提供查找類!

    引導方法(Bootstrap Method):

      存放在BootstrapMethods屬性中,是有固定參數,並且返回值是java.lang.invoke.CallSite對象的方法。

      代表真正要執行的目標方法調用。

    根據CONSTANT_InvokeDynamic_info中的信息,虛擬機找到並執行引導方法,得到一個CallSite對象,

    最終使用CallSite調用目標方法。

    現在有了方法的標識,誰去幫我們找呢?

    MethodHandles.Lookup lookup() :

      Returns a Lookup object with full capabilities to emulate all supported bytecode behaviors of the caller.

      Lookup對象可以模擬調用的字節碼行為。就是它了。

   

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

  主要注意,基於操作數棧,數據交換都要經過操作數棧。指令也是針對棧元素進行操作的。

  

  

 

 

 

  

 

 

  

    

 

 

    


免責聲明!

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



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