1. 程式人生 > >JVM(十二):方法呼叫

JVM(十二):方法呼叫

JVM(十二):方法呼叫

在 JVM(七):JVM記憶體結構 中,我們說到了方法執行在何種記憶體結構上執行:Java 方法活動在虛擬機器棧中的棧幀上,棧幀的具體結構在記憶體結構中已經詳細講解過了,下面就讓我們來看一下 方法是如何呼叫的。

方法呼叫

首先,我們要明白一個基礎性概念:方法呼叫並不是方法執行。其只是確定該呼叫哪一個方法而已(多型的影響,選擇方法的不同版本)。並且因為 Java 呼叫的動態性,有些方法需要在類載入階段動態解析,這也為 JVM 解析符號引用成直接引用提供了難度。

解析

在 JVM(五):探究類載入過程-上 中,我們說過在類載入過程的解析階段, JVM 會將符號引用轉變為直接引用。但這需要方法在程式真正執行的之前就有一個 可確定 的版本,且這個版本在整個執行期間是不可更改的。這種在類載入階段就能解析出的方法呼叫被稱為 解析。

解析條件

滿足解析條件的方法在 Java 語言中有 靜態方法 和 私有方法 兩類。前者與類型別直接關聯,後者在外部無法被訪問。這種特性也使得其不可能通過繼承或別的方式來重寫其版本,因此適合在解析階段就進行處理。

在 JVM 內部與之相對應的就是invokestaticinvokespecial 兩種位元組碼指令。這兩種指令對應的就是 呼叫靜態方法 、呼叫例項構造器。 在執行這四類方法時,在類載入的解析階段就會將符號引用直接轉變為直接引用。

上述的這些方法也被稱為 非虛方法 。

滿足載入階段解析的還有一個特殊的方法為 final方法,因為其無法被覆蓋,也沒有其他版本,因此雖然其是用 invokevirtual

來呼叫,其也是一種 非虛方法,可以在編譯期間就完全確定。

分派

分派則是在程式執行的期間才能確定呼叫哪個版本的方法。其也揭示了 過載 和 重寫 的本質。下面就讓我們來看一下其在 JVM 內是如何實現的。

靜態分派

// 程式碼參考自<<深入理解Java虛擬機器>>
public class Demo {
    static class Human{

        public void overWriteDemo(){
            System.out.println("human overWriteDemo");
        }

    }
    static class Man extends Human {
        @Override
        public void overWriteDemo(){
            System.out.println("Man overWriteDemo");
        }

    }
    static class Woman extends Human{
        @Override
        public void overWriteDemo(){
            System.out.println("Woman overWriteDemo");
        }

    }

    public  void sayHello(Human human){
        System.out.println("human");
    }
    public  void sayHello(Man man){
        System.out.println("Man");
    }
    public  void sayHello(Woman woman){
        System.out.println("Woman");
    }

    public static void main(String[] args) throws InterruptedException {
        Human man = new Man();
        Human woman = new Woman();
        Demo demo = new Demo();
        demo.sayHello(man);
        demo.sayHello(woman);
        }
}

首先各位讀者可以看一下以下程式碼輸出的是什麼。

讀者可以帶著您這邊的答案來看下一些概念。上面的 Human 我們稱之為變數的 靜態型別,或者叫做變數的 外觀型別 。後面的 Man 或 Women 我們則稱之為變數的 實際型別。

對應這兩種型別,變數的靜態型別是本身不變的,只有在使用的時候才可以對其進行改變使用,而且最終的靜態型別在編譯期就可知,如

    demo.sayHello((Man)man);
    demo.sayHello((Women)man);

這樣就可以在使用的時候,使用指定的型別,但其變數本身的屬性還是不會更改。

這種語法對應就是 Java 語言中的 過載 語法,虛擬機器是通過引數的 靜態型別 來確定該呼叫哪個版本的方法。

現在回到程式碼輸出結果上,第一次程式碼輸出的結果為

Human
Human

第二次程式碼輸出結果為:

Man
Women

動態分派

// 為了節約篇幅,前面的內容就不復制了,只是在呼叫的時候更改了一下程式碼,如下所示:
public static void main(String[] args) throws InterruptedException {
        Human man = new Man();
        Human woman = new Woman();
      
        man.overWriteDemo();
        woman.overWriteDemo();
        
        man = new Woman();
        man.overWriteDemo();
        }

現在讀者再來看一下以上這段程式碼的輸出結果是什麼,帶著心中的答案我們再來看一下 動態分派 的概念。

通過javap分析其位元組碼錶現,我們發現其呼叫的是invokevirtual指令 , 而invokevirtual在呼叫的時候,執行時解析步驟為以下方式:

  1. 找到元素找到的實際型別,記為 C
  2. 如果在型別 C 中找到簽名相符的方法 , 則進行許可權校驗。 如果通過 , 則返回其對應引用 , 如果沒通過 , 則丟擲異常;
  3. 在第二步中 , 如果沒找到相符的方法 , 則從 C 從下而上地對其父類進行查詢和驗證;
  4. 如果仍舊沒有找到 , 則丟擲異常 .

通過以上者多層的步驟推進 , 則可以找到實際呼叫的物件型別,從而決定呼叫哪個版本的方法。

實現方式

上面我們看到了動態多型的實現邏輯,現在我們來看一下在 JVM 內是如何實現這個過程的.

首先,我們看到該過程需要逐層搜尋以找到對應類。但是如果在實際呼叫過程中,這樣逐層的搜尋是會影響效能的。因此,面對這種情況,JVM 採用了虛方法表 的形式來實現。在虛方法表中儲存了每個方法的實際地址。

方法執行

Java 執行方法分為 解釋執行(通過直譯器執行) 和 編譯執行(通過編譯器編譯為原生代碼執行)兩種。

在本文中,我們著重講一下在解釋執行的過程中,JVM 內部是如何實現的。至於編譯執行,因為還涉及到 JIT 的優化等概念。放在後面的 編譯優化 章節再詳細敘說。

基於棧的執行模型

方法執行的過程其實就是在執行 Class 中位元組碼的指令集部分,而從 # JVM(六):探究類載入過程-下 中,我們已經看到指令集的大部分組成部分,都是零地址指令。那麼什麼是零地址指令呢,下面給大家舉個例子:

iconst_1
iconst_1
iadd
istore_0

以上這段程式碼就是一段零地址指令,其含義為計算1+1的結果,具體操作過程為:

  1. 首先將 1 入棧
  2. 將另一個 1 入棧
  3. 將棧頂的兩個元素相加,並將計算結果重新入棧
  4. 將棧頂元素(結果),放入區域性變量表中 slot0 位置

基於暫存器的執行模型

而另外還有一種基於暫存器的執行方式,比如上面這段程式碼如果用暫存器的方式來實現就是如下所示:

mov eax,1
add eax,1

這段指令集的內容就是:

  1. 將 EAX 暫存器的值設為 1;
  2. add 指令將eax的值+1,然後結果就儲存在eax暫存器上。

執行模型對比

我們很難說清楚兩種設計理念哪個更加的好,因為在現如今,兩套指令集都在茁壯發展中,因此肯定都有各自的優點,接下來就讓我們看一下其各自的優點是什麼。

首先既然是基於棧的,那麼其最大的優點應該就是 可移植,因為暫存器是與硬體息息相關的,其會受到硬體暫存器的數量等原因限制;其次在棧結構中,大部分指令都是零地址的,那麼其第二個優點就是 程式碼更加的緊湊,而暫存器這樣的指令集還需要帶上多個引數;最後基於棧的模型,編譯器 實現起來也會更加簡單,因為所有的操作都是在棧上進行,不需要考慮記憶體空間分配的問題。

而對暫存器模型來說,其最大的優點就是 執行速度塊,因為基於棧來操作的話,同樣的操作,基於棧要比基於暫存器來說, 多出大量的指令內容,並且需要頻繁的 出棧入棧操作,也就是大量的記憶體訪問操作, 這都會影響指令執行的效率。而這也是所有主流物理機都使用基於暫存器來實現指令集的原因。

總結

在本文中,我們講述了方法呼叫和方式執行的過程,與# JVM(七):JVM記憶體結構 中方法執行時涉及到的記憶體結構相結合,這三者,使我們詳細理清了 Java 程式執行一個方法的全過程,希望讀者們以後在寫程式碼中,可以在腦海中清晰地明白這個過程。

文章在公眾號「iceWang」第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!

本系列文章主要借鑑自《深入分析 JavaWeb 技術內幕》和《深入理解 Java 虛擬機器-JVM 高階特性與最佳實踐》。