1. 程式人生 > >從位元組碼指令看重寫在JVM中的實現

從位元組碼指令看重寫在JVM中的實現

        Java是解釋執行的,包括動態連結的特性,都給解析或執行期間提供了很多靈活擴充套件的空間。面嚮物件語言的繼承、封裝和多型的特性,在JVM中是怎樣進行編譯、解析,以及通過位元組碼指令如何確定方法呼叫的版本是本文如下要探討的主要內容,全文圍繞一個多型的簡單舉例來看在JVM中是如何實現的。

        先簡單介紹幾個概念。對於位元組碼執行模及位元組碼指令集的相關概念可以參考之前的一篇介紹:JVM位元組碼執行模型及位元組碼指令集

一、方法呼叫的解析

        在Class檔案中,方法呼叫是常量池中的一個符號引用,在載入的解析期或者執行時才能確定直接引用。

        下面兩種指令是在解析期就可以確定直接引用,呼叫的對應方法也叫作非虛方法。

1、  invokestatic 主要用於呼叫靜態方法

2、  invokespecial 主要用於呼叫私有方法,構造器,父類方法。

        下面兩種是在執行時才能確定直接引用的,但是除了final方法,final方法也可以在解析期確定方法的呼叫版本。

1、  invokevirtual 虛方法,不確定呼叫那一個實現類

2、  invokeinterface 介面方法,執行時才能確定實現介面的物件。

二、動態分派

          先回顧一下靜態和動態分派的概念。Java中,所有以靜態型別來定位方法執行版本的分派動作,都稱為靜態分派。其實也就是過載(Overload)就是一種典型的靜態分派,在編譯期就可以知道方法呼叫的實際版本。相對得,動態分派是需要在執行期才能確定方法的版本,也就是直接引用,一種典型應用就是重寫(OverWrite)。在呼叫invokevirtual指令時,把常量池中的類方法符號引用解析到直接引用的過程就是重寫的過程,執行期根據實際型別確定方法的執行版本。

三、解釋執行

         Java的解釋執行機制,使jaavc編譯的過程涵蓋了從程式程式碼的語法、詞法分析,再到AST(抽象語法樹)生成線性的位元組碼指令流的過程。而解釋執行是在JVM內部,基於棧的指令集提供了整個平臺的可移植性支撐,所以這也是java執行慢的要因,因為不像編譯執行,過程中需要更多的出入棧指令,而棧又是記憶體的一個區塊,對記憶體的頻繁訪問降低了效能。

四、重寫(OverWrite)舉例

          下面通過一個簡單的例子來看一下重寫在JVM中的位元組碼執行模型。

父類:

package bytecode;

/**
 * Created by yunshen.ljy on 2015/7/27.
 */
public class Wine {

    public String drink(int ml) {
        return "drink " + ml + "ml wine";
    }

}

子類:

package bytecode;
/**
 * Created by yunshen.ljy on 2015/7/27.
 */
public class Beer extends Wine{

    /**
     * 重寫父類方法,實現多型
     */
    public String drink(int ml){
        return "drink " + ml +"ml beer";
    }
}

呼叫:

package bytecode;

/**
 * Created by yunshen.ljy on 2015/7/26.
 */
public class MethodInvotionTest {

    public String drink(int ml) {

        Wine wines = new Beer();
        return wines.drink(ml);

    }
}

        我們都知道,方法呼叫返回的結果是“drink XX ml beer”。但是在編譯期,位元組碼中的指令是無法確定實際呼叫的方法版本的。下面看一下呼叫方法的位元組碼結構。

public java.lang.String drink(int);
    descriptor: (I)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #2                  // class bytecode/Beer
         3: dup
         4: invokespecial #3                  // Method bytecode/Beer."<init>":()V
         7: astore_2
         8: aload_2
         9: iload_1
        10: invokevirtual #4                  // Method bytecode/Wine.drink:(I)Ljava/lang/String;
        13: areturn
      LineNumberTable:
        line 10: 0
        line 12: 8

         第一條指令new 建立物件,把引用入棧指令,new 指令後面的#4就是前文提到的,對於執行時常量池的一個引用,只是javap命令處理成比較易懂的方式來顯示。接著,後面的dup 指令複製剛放入的引用(運算元棧棧頂的值的複製並且將這個“副本”放到棧頂)。Invokespecial 指令就是之前介紹的非虛方法呼叫指令,在運算元棧中通過其中的一個引用呼叫Beer的構造器,初始化物件,讓另一個相同引用指向初始化的物件,然後前一個引用(this)彈出棧。astore_2把引用儲存到區域性變量表中的索引2位置中。aload_2 把剛才區域性變量表中索引2處的值壓入操作棧。iload_1將int引數,也就是區域性變量表中索引1處的值壓入運算元棧。Invokevirtual指令將執行運算元棧中的兩個值出棧,執行方法呼叫,指令後的#4只是常量池中的一個符號引用,只有在執行時,才能確定方法的直接引用。最後的areturn語句將方法執行結果的值出棧,消除當前的棧幀,如果上層有對於當前方法的繼續呼叫,那麼會將呼叫的方法的棧幀設定成當前棧幀(current stack frame)。

       下面通過對比每條指令執行後,區域性變量表和運算元棧中的值來加強一下上述的出棧、入棧操作。首先通過stack=2, locals=3, args_size=2 ,先知道了方法的區域性變量表是佔用了3個slot,運算元棧的size是2個slot。而因為我們的方法是例項方法(非類方法),所以隱含的this關鍵字的指令我們先描述到區域性變量表的第一個位置,佔用一個slot。(以下不再畫出常量池的結構,可參照calss檔案自行分析)

 








        這裡只是個簡單的舉例,和一些知識的回顧。知道JVM的執行引擎和位元組碼指令的一些概念,會讓我們對於程式執行的結果預期更加準確,也更易理解一些java語言版本的設計模式實現。這裡只是拋磚引玉,對於其他幾種方法呼叫的指令,也建議大家嘗試進行更多的分析。