JAVA方法調用中的解析與分派

来源:https://www.cnblogs.com/hapjin/archive/2018/07/27/9374269.html
-Advertisement-
Play Games

JAVA方法調用中的解析與分派 本文算是《深入理解JVM》的讀書筆記,參考書中的相關代碼示例,從位元組碼指令角度看看解析與分派的區別。 方法調用,其實就是要回答一個問題:JVM在執行一個方法的時候,它是如何找到這個方法的? 找一個方法,就需要知道 所謂的 地址。這個地址,從不同的層次看,對它的稱呼也不 ...


JAVA方法調用中的解析與分派

本文算是《深入理解JVM》的讀書筆記,參考書中的相關代碼示例,從位元組碼指令角度看看解析與分派的區別。

方法調用,其實就是要回答一個問題:JVM在執行一個方法的時候,它是如何找到這個方法的?

找一個方法,就需要知道 所謂的 地址。這個地址,從不同的層次看,對它的稱呼也不同。從編譯器javac的角度看,我稱之為符號引用;從jvm虛擬機角度看,稱之為直接引用。或者說從class位元組碼角度看,將這個地址稱之為符號引用;當將class位元組碼載入到記憶體(方法區)中後,稱之為直接引用。當然,這是我個人的理解,也許不正確。

從符號引用如何變成直接引用的?

在回答這個問題之前,先看看符號引用是什麼?它是怎麼來的?為什麼需要它?直接引用又是什麼?最後,符號引用是怎麼轉化成直接引用的。

  1. 符號引用是什麼?

    根據定義:符號引用屬於編譯原理方面的概念,包括了下麵三類常量:

    • 類和介面的全限定名
    • 欄位的名稱和描述符
    • 方法的名稱和描述符

    拋開定義,舉個例子來說明:工程師寫的一個JAVA程式如下:

    package org.hapjin.dynamic;
    
    /**
     * Created by Administrator on 2018/7/26.
     */
    public class SymbolicTest {
        private int m;
        public void test(){}
    }

    源代碼經過javac編譯後生成的class文件,這個class文件當然也是按規定的格式組織的,即class文件格式。使用WinHex打開如下,然後來找一找 類的全限定名,在class文件中的哪個地方。

如上圖,藍色陰影區域(紅色方框)區域中標出了:SymbolicTest.java 這個類的全限定名:!Lorg/hapjin/dynamic/SymbolicTest,而這就是一個符號引用。這樣就明白了符號引用是怎麼來的了。

  1. 為什麼需要符號引用?

    符號引用其實是從位元組碼角度來標識類、方法、欄位。位元組碼只有載入到記憶體中才能運行,載入到記憶體中,就是記憶體定址了。

    在class文件中不會保存各個方法、欄位的最終記憶體佈局信息,因此這些欄位、方法的符號引用不經過運行期轉換的話無法得到真正的記憶體入口地址,也就無直接被虛擬機使用。

那這個運行期轉換,到底是在類的生命周期的哪個階段進行的轉換?是在載入階段、還是在連接階段、還是在初始化階段、還是在使用階段?這個後面再分析。 ​

  1. 直接引用是什麼?

    JAVA虛擬機運行時數據區 分為很多部分:

其中有一個叫做方法區,它用於存儲已被虛擬機載入的類信息、常量、靜態變數……比如說,類的介面的全限定名、方法的名稱和描述符 這些都是類信息。因此,是被載入到方法區存儲。

那前面已經提到,類的介面的全限定名、方法的名稱和描述符 都是符號引用,當被載入到記憶體的方法區之後,就變成了直接引用(這樣說,有點絕對,因為 有些方法需要等到jvm執行位元組碼的時候,或者叫程式運行的時候 才能知道要調用哪個方法)

直接引用有兩種方式來定位對象,句柄和直接指針。看下麵的圖加深下理解:

虛擬機棧裡面 reference 可以理解成直接引用,換句話說,直接引用 存儲 在虛擬機棧中(並不是說,其它地方就不能存儲直接引用了,因為我也不知道其他地方能不能存儲直接引用,比如 static 類型的對象的直接引用)。

從這裡也可以映證一點:在記憶體分配與回收過程中,判斷對象是否可達的可達性分析演算法中:可作為GC roots 的對象有:虛擬機棧中引用的對象。

對符號引用和直接引用有了一定認識之後,最後來看看:符號引用是如何變成直接引用的?先來看張圖:

類從被載到虛擬機記憶體,到卸載出記憶體為止,整個生命周期如上圖。那有些 符號引用轉化成直接引用,是不是也發生在上面某個階段呢?

其實就是根據 在哪個階段 符號引用 轉化成直接引用,將方法調用分成:解析調用 與 分派調用。

在類載入的解析階段,會將一部分符號引用轉化為直接引用,這種解析能成立的前提是:方法在程式真正運行之前就有一個可確定的調用版本,並且這個方法的調用版本在運行期是不可改變的。

換句話說,調用目標在程式代碼寫好、編譯器進行編譯時就必須確定下來,這類方法的調用稱為解析

只要能被 invokestatic 和 invokespecial 指令調用的方法,都可以在解析階段中確定唯一的調用版本,符合這個條件的方法有:靜態方法、私有方法、實例構造器、父類方法 4類。

下麵來看下,這四類方法 調用的位元組碼指令和符號引用是啥?

public class StaticResolution {
    public static void sayHello() {
    System.out.println("hello world");
    }

    private void sayBye() {
    System.out.println("bye");
    }

    public static void main(String[] args) {
    StaticResolution.sayHello();//靜態方法調用

    StaticResolution sr = new StaticResolution();
    sr.sayBye();//私有方法調用
    }
}

使用javap -v StaticResolution 對class文件反編譯,查看main方法的內容如下:

  • 序號0 是靜態方法的調用

    這個靜態方法的描述符 是sayHello:()V,由於靜態方法是與類相關的,不能在一個類裡面再定義一個與描述符sayHello:()V一樣的方法,不然編譯期就會提示“重名的方法”錯誤。(雖然可以通過修改位元組碼的方式,在同一個class位元組碼文件裡面可存在2個方法描述符相同的方法,但是在類載入的驗證階段,就會驗證失敗,具體可參考從虛擬機指令執行的角度分析JAVA中多態的實現原理中提到的方法描述符與特征簽名的區別)

    所以,虛擬機在執行 invokestatic 這條位元組碼指令的時候,能夠根據sayHello:()V方法描述符(符號引用) 來唯一確定調用的方法就是public static void sayHello() {System.out.println("hello world");}

  • 序號7 是實例方法的調用(預設構造函數的調用)

  • 序列12 是私有方法的調用

    同理,由於私有方法不能被子類繼承,因此在同一個類裡面也不能再定義一個與描述符sayBye:()V一樣的方法。

因此,上面四類方法的調用稱為 解析調用,對於這四類方法,它們的符號引用在 解析階段 就轉成了 直接引用。另外其實可以看出,解析調用的方法接收者是唯一確定的。

下麵再來看分派調用:

用重載和覆蓋來解釋分派調用,可參考從虛擬機指令執行的角度分析JAVA中多態的實現原理 。後面的講解也以這篇參考文章中的 圖一 和 圖二 進行說明。

分派調用分成兩類:靜態分派和動態分派。其中,重載屬於靜態分派、方法覆蓋屬於動態分派。下麵來解釋一下為什麼?

在分派中,涉及到一個概念:叫實際類型 和 靜態類型。比如下麵的語句:

    Human man = new Man();
    Human woman = new Woman();

等式左邊叫靜態類型,等式右邊是實際類型。比如 man 這個引用,它的靜態類型是Human,實際類型是Man;woman這個引用,靜態類型是Human,實際類型是Woman

從參考文章的圖一和圖二中看出:sayHello方法的調用都是由 invokevirtual 指令執行的。我想,這也是解析與分派的一個區別吧 ,就是分派調用是由 invokevirtual 指令來執行。

那靜態分派調用 和 動態分派調用的區別在哪兒呢?

  • 靜態分派

    靜態分派方法的調用(方法重載)如下:

        sr.sayHello(man);//hello, guy
        sr.sayHello(woman);//hello, guy

    man引用和woman引用的靜態類型都是Human,因此方法重載是根據 引用的靜態類型來選擇相應的方法執行的,也就是說:上面兩條語句中的sayHello(Human )方法的參數類型都是Human,結果就是選擇了參數類型為 Human 的 sayHello方法執行。【這樣也是不是可以說:對於 方法參數 這種類型的引用,只有靜態類型,沒有實際類型?】

          Human man = new Man();// man 是“語句類型的引用”
    
          public void sayHello(Human human){}//human 是 sayHello方法的參數,稱之為 參數類型 的引用
  • 動態分派

    動態分派方法調用(方法覆蓋)的代碼如下:

        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();//man say hello
        woman.sayHello();//woman say hello

    由上面可知:變數man引用的動態類型是Man,變數woman引用的動態類型是Woman,方法的執行是根據引用的 實際類型來選擇相應的方法執行的。結果就是分別選擇了 Man類的sayHello方法 和 Woman類的sayHello方法執行。

當然了,靜態分派與動態分派的具體執行過程的差異也可以由參考文章窺出端倪。

至此,解析與分派就介紹完了。

最後,書中使用QQ和_360 的示例,談到了JAVA語言的靜態分派屬於多分派類型;動態分派屬於單分派類型。趁著前面對分派的分析,記錄一下我的理解:

首先,它是根據宗量的個數來區分單分派與多分派的。那宗量是什麼呢?宗量可理解成:引用的靜態類型、實際類型、方法的接收者。看代碼:

public class Dispatch {
    static class QQ{}
    static class _360{}

    public static class Father{
    public void hardChoice(QQ arg)
    {
        System.out.println("father choose qq");
    }
    public void hardChoice(_360 arg)
    {
        System.out.println("father choose 360");
    }
    }

    public static class Son extends Father{
    public void hardChoice(QQ arg)
    {
        System.out.println("son choose qq");
    }
    public void hardChoice(_360 arg)
    {
        System.out.println("son chooes 360");
    }
    }

    public static class Son2 extends Father{
    public void hardChoice(QQ arg)
    {
        System.out.println("son2 choose qq");
    }
    public void hardChoice(_360 arg)
    {
        System.out.println("son2 chooes 360");
    }
    }

    public static void main(String[] args) {
    Father father = new Father();
    Father son = new Son();
    Father son2 = new Son2();

    father.hardChoice(new _360());//father choose 360
    son.hardChoice(new QQ());//son choose qq
    son2.hardChoice(new QQ());//son2 choose qq
    son2.hardChoice(new _360());//son2 chooes 360
    }
}

javap -v Dispatch 反編譯出來class位元組碼文件的main方法如下:

其中下麵這兩句方法調用的符號引用是一樣的,都是:org/hapjin/dynamic/Dispatch$Father.hardChoice:(Lorg/hapjin/dynamic/Dispatch$QQ;)V

    son.hardChoice(new QQ());//son choose qq
    son2.hardChoice(new QQ());//son2 choose qq

既然這兩個方法調用的符號引用是一樣,但是它們最終輸出了不同的值。說明,虛擬機在執行的時候,選擇了不同的方法來執行。而變數son 和 son2 的靜態類型都是Father,但是son的實際類型是 類Son,son2的實際類型是 類Son2。(變數son和son2 都是它們各自方法的接收者)

而書中說:“因為這裡參數的靜態類型、實際類型都對方法的選擇不會構成任何影響”,其實在編譯出class位元組碼文件的時候,方法的參數的類型就已經確定了,在這個示例中都是 類QQ,那當然不能構成影響了,但我總覺得這種說法有點勉強,導致費解。

動態分派不僅要看方法接收者的實際類型,也是要看方法的參數類型的,只是編譯成class文件的時候方法的參數類型就已經確定了而已。其實也不用管,只需要明白 invokevirtual 指令解析過程的大致步驟就能區分,方法在運行時到底是調用哪個方法了。

invokevirtual指令的解析過程大致分為以下幾個步驟:

1. 找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C
2. 如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問許可權校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。
3. 否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
4. 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

而這兩句方法調用的符號引用也是一樣的,都是:org/hapjin/dynamic/Dispatch$Father.hardChoice:(Lorg/hapjin/dynamic/Dispatch$_360;)V

father.hardChoice(new _360());//father choose 360
son2.hardChoice(new _360());//son2 chooes 360

但是,這兩句的執行結果也不一樣,根據invokevirtual指令的解析過程可知:

father.hardChoice(new _360());語句操作數棧頂的第一個元素所指的對象的實際類型是Father。

son2.hardChoice(new _360());語句操作數棧頂的第一個元素所指的對象的實際類型是Son2。

所以它們一個執行的是Father類中的hardChoice(_360 arg),一個執行的是Son2類中的hardChoice(_360 arg)方法。

總結一下:虛擬機具體在選擇哪個方法執行時:

根據在編譯成class位元組碼文件後就確定了執行哪個方法----解析 or 分派

根據在方法是否由位元組碼指令 invokevirtual 調用----解析 or 分派(分派調用是由 invokevirtual 指令執行的)

根據方法接收者的靜態類型 和 實際類型 ---- 動態分派 or 靜態分派

根據宗量個數來 確定具體執行哪個方法----多分派 or 單分派

但總感覺這樣劃分有點絕對,不太準確。

構思了一個星期的文章,終於完成了。

參考文章:從虛擬機指令執行的角度分析JAVA中多態的實現原理

參考書籍:《深入理解java虛擬機》

原文:https://www.cnblogs.com/hapjin/p/9374269.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 運行結果: ...
  • 最近在面試過程中,遇到許多抽象類和介面的面試題,所以今天特意研究了下,然後寫出來分享給大家,希望對面試的朋友有幫助,如果覺得寫的可以點個贊吧! 1:抽象類可以實例化,抽象類可以通過子類間接的實例化父類,介面不能實例化。 2:抽象類可以擁有私有屬性、方法,介面不能擁有。 3:抽象類方法不能使用defa ...
  • C++11新特性: auto: auto讓編譯器通過初始值來推算變數的類型。 auto定義的變數必須有初始值。 auto聲明的所有變數的初始基本數據類型都必須一樣。 decltype: decltype的作用是選擇返回操作數的數據類型。 編譯器分析表達式並得到它的類型,卻不計算表達式的值。 如果de ...
  • Given a string, find the length of the longest substring without repeating characters. Examples: Given "abcabcbb", the answer is "abc", which the leng ...
  • 15.1 動態代理 在之後學習Spring框架時,Spring框架有一大核心思想,就是AOP,(Aspact-Oriented-Programming 面向切麵編程) 而AOP的原理就是Java的動態代理機制,在Java的動態代理機制中,有兩個重要的類或介面,一個是 InvocationHandle ...
  • 【前言】對於像我一樣的新手來說,我覺得此環節難點主要是相關依賴包的安裝和Flask-SQLAlchemy的使用,下麵將一一講解: 所謂數據模型,百度的解釋是:“數據模型(Data Model)是數據特征的抽象。數據(Data)是描述事物的符號記錄,模型(Model)是現實世界的抽象。數據模型從抽象層 ...
  • 3-1.c指針用作函數參數 目的:是為了通過swapdate()函數把實參x,y的值進行交換,上述例子是將形參dat_x,dat_y的值進行交換,但是形參的交換並沒有改變實參的交換,因為函數在調用時給形參分配了單獨的記憶體空間,實參的值傳遞給形參實際是把實參的值放在形參的記憶體空間,形參的值是實參的備份 ...
  • 閏年計算器 題目:輸入年份,判斷該年是否為閏年。 方法:1.能被400整除的年份 2.能被4整除,但不能被100整除 註:以上案例主要涉及到了條件判斷if...else...以及關係運算符的知識點。 每月天數計算器 題目:輸入一個月份,判斷該月有多少天 方法:先判斷年份是否為閏年或平年,再計算該年份 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...