JVM學習筆記——節碼執行引擎

午夜12點發表於2018-06-28

簡述

執行引擎是Java虛擬機器最核心的組成部分之一,,所有的Java虛擬機器的執行引擎都是一致的:
輸入:位元組碼檔案
處理:位元組碼解析
輸出:執行結果

執行時棧幀結構

在介紹虛擬機器棧時就提到,每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。棧幀中需要多大的區域性變數表和多深的運算元棧在編譯程式碼的過程中已經完全確定,並寫入到方法表的Code屬性中。在活動的執行緒中,位於當前棧頂的棧幀才是有效的,執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作。

JVM學習筆記——節碼執行引擎

方法呼叫

一切方法呼叫在Class檔案裡面都是一個常量池中的符號引用。在類載入解析階段,可能會將其中一部分符號引用轉化為直接引用,也有可能會在初始化階段之後再開始,取決於方法在執行之前是否有有確定的呼叫版本,且在執行期間不變。

在Java中符合編譯器可知,執行期不可變的方法,主要包括靜態方法和私有方法兩大類,它們都不可能通過某些方式重寫其他版本,因此它們都適合在類載入階段進行解析

JVM提供了5條方法呼叫位元組碼:
invokestatic:呼叫靜態方法
invokespecial:呼叫例項構造器方法、私有方法和父類方法
invokevirtual:呼叫所有的虛方法
invokedynamic:執行時動態解析出呼叫點限定符所引用的方法,再執行該方法
只要能被invokestaticinvokespecial指令呼叫的方法,都可以在解析階段中確定唯一的呼叫版本,它們在類載入時會把符號引用解析為該方法的直接引用。

示例:

  • invokestatic
  • 
    public class Person {
        public static void sayHello() {
            System.out.println("hello world");
        }
        public static void main(String[] args) {
            sayHello();
        }
    }
    複製程式碼
    javap -verbose檢視

    JVM學習筆記——節碼執行引擎

  • invokespecial
  • 
    public class Person {
        public static void main(String[] args) {
            new Person();
        }
    }
    複製程式碼
    javap -verbose檢視

    JVM學習筆記——節碼執行引擎

    通過invokestatic和invokespecial指令呼叫的方法稱為非虛方法,與之相反,其他方法稱為虛方法(除去final方法),虛方法的呼叫是一個分派的過程,有靜態也有動態,可分為靜態單分派、靜態多分派、動態單分派和動態多分派。

    靜態分派

    所有依賴靜態型別來定位方法執行版本的分派,靜態分派的典型應用是方法過載,靜態分派發生在編譯階段,因此靜態分派的動作不由虛擬機器執行。eg:

    
    public class Person {
        public void sayHello(Object obj){
            System.out.println("hello Object");
        }
        public void sayHello(String str){
            System.out.println("hello String");
        }
        public static void main(String[] args) {
            Person p = new Person();
            Object obj = new String();
            p.sayHello(obj);
        }
    }
    複製程式碼

    結果:

    
        hello Object
    複製程式碼

    相對於變數obj,Object是其靜態變數,String是其實際變數,在編譯階段,Java編譯器會根據引數的靜態型別決定呼叫哪個過載版本。用javap -verbose再看下

    JVM學習筆記——節碼執行引擎

    動態分派

    動態分派根據實際型別確定方法執行版本

    
    public class Person {
        public static void main(String[] args) {
            Object p = new Person();
            Object obj = new String("比利時");
            System.out.println(obj.toString());
            System.out.println(p.toString());
        }
    }
    複製程式碼

    輸出

    
    比利時
    jvm.Person@63ce0e18
    複製程式碼

    同一個靜態型別,呼叫toString方法,結果完全不同,原因就是因為這兩個變數的實際型別不同。通過java -verbose 檢視位元組碼指令

    JVM學習筆記——節碼執行引擎
    21、31行指令將之前存放到區域性變數表1、2位置的物件引用(接受者)壓入運算元棧的棧頂,22、32行是方法呼叫指令,雖然指令一樣都是Object.toString,但是這兩個指令最終執行的目標方法不相同
    ①.找到運算元棧的棧頂元素所指向的物件的實際型別,記為C
    ②.如果C中存在描述符和簡單名稱都相符的方法,則進行訪問許可權驗證,如果驗證通過,則直接返回這個方法的直接引用,否則返回java.lang.IllegalAccessError異常
    ③.如果C中不存在對應的方法,則按照繼承關係對C的各個父類進行第2步的操作
    ④.如果各個父類也沒對應的方法,則丟擲異常
    所以上述兩次invokevirtual指令將相同的符號引用解析成了不同物件的直接引用,這個過程就是Java語言中重寫的本質

    如何實現?
    為類在方法區中建立虛方法表,虛方法表中存放著各個方法的實際入口地址,如果某個方法在子類中沒有被重寫,那子類的虛方法表中的地址入口和父類相同方法的地址入口一致,都指向父類的實現入口。

    JVM學習筆記——節碼執行引擎
    以上面程式碼為例,Person類沒有重寫toString()方法,所以toString()方法指向Object型別資料,而String重寫了toString()方法,所以沒有(未列Object其他方法)。

    感謝

    《深入理解Java虛擬機器》

    相關文章