日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JAVA方法调用中的解析与分派

發(fā)布時(shí)間:2025/3/15 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JAVA方法调用中的解析与分派 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

JAVA方法調(diào)用中的解析與分派

本文算是《深入理解JVM》的讀書筆記,參考書中的相關(guān)代碼示例,從字節(jié)碼指令角度看看解析與分派的區(qū)別。

方法調(diào)用,其實(shí)就是要回答一個(gè)問題:JVM在執(zhí)行一個(gè)方法的時(shí)候,它是如何找到這個(gè)方法的?

找一個(gè)方法,就需要知道 所謂的 地址。這個(gè)地址,從不同的層次看,對(duì)它的稱呼也不同。從編譯器javac的角度看,我稱之為符號(hào)引用;從jvm虛擬機(jī)角度看,稱之為直接引用?;蛘哒f從class字節(jié)碼角度看,將這個(gè)地址稱之為符號(hào)引用;當(dāng)將class字節(jié)碼加載到內(nèi)存(方法區(qū))中后,稱之為直接引用。當(dāng)然,這是我個(gè)人的理解,也許不正確。

從符號(hào)引用如何變成直接引用的?

在回答這個(gè)問題之前,先看看符號(hào)引用是什么?它是怎么來的?為什么需要它?直接引用又是什么?最后,符號(hào)引用是怎么轉(zhuǎn)化成直接引用的。

  • 符號(hào)引用是什么?

    根據(jù)定義:符號(hào)引用屬于編譯原理方面的概念,包括了下面三類常量:

    • 類和接口的全限定名
    • 字段的名稱和描述符
    • 方法的名稱和描述符

    拋開定義,舉個(gè)例子來說明:工程師寫的一個(gè)JAVA程序如下:

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

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

  • 如上圖,藍(lán)色陰影區(qū)域(紅色方框)區(qū)域中標(biāo)出了:SymbolicTest.java 這個(gè)類的全限定名:!Lorg/hapjin/dynamic/SymbolicTest,而這就是一個(gè)符號(hào)引用。這樣就明白了符號(hào)引用是怎么來的了。

  • 為什么需要符號(hào)引用?

    符號(hào)引用其實(shí)是從字節(jié)碼角度來標(biāo)識(shí)類、方法、字段。字節(jié)碼只有加載到內(nèi)存中才能運(yùn)行,加載到內(nèi)存中,就是內(nèi)存尋址了。

    在class文件中不會(huì)保存各個(gè)方法、字段的最終內(nèi)存布局信息,因此這些字段、方法的符號(hào)引用不經(jīng)過運(yùn)行期轉(zhuǎn)換的話無法得到真正的內(nèi)存入口地址,也就無直接被虛擬機(jī)使用。

  • 那這個(gè)運(yùn)行期轉(zhuǎn)換,到底是在類的生命周期的哪個(gè)階段進(jìn)行的轉(zhuǎn)換?是在加載階段、還是在連接階段、還是在初始化階段、還是在使用階段?這個(gè)后面再分析。 ?

  • 直接引用是什么?

    JAVA虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū) 分為很多部分:

  • 其中有一個(gè)叫做方法區(qū),它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量……比如說,類的接口的全限定名、方法的名稱和描述符 這些都是類信息。因此,是被加載到方法區(qū)存儲(chǔ)。

    那前面已經(jīng)提到,類的接口的全限定名、方法的名稱和描述符 都是符號(hào)引用,當(dāng)被加載到內(nèi)存的方法區(qū)之后,就變成了直接引用(這樣說,有點(diǎn)絕對(duì),因?yàn)?有些方法需要等到j(luò)vm執(zhí)行字節(jié)碼的時(shí)候,或者叫程序運(yùn)行的時(shí)候 才能知道要調(diào)用哪個(gè)方法)

    Class 文件的常量池中存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另一部分將在每次運(yùn)行期間轉(zhuǎn)化為直接引用,稱為動(dòng)態(tài)連接(動(dòng)態(tài)分派)。棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū):虛擬機(jī)棧(不同于堆、方法區(qū))中的內(nèi)容,棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、和方法返回地址等信息。這里所說的動(dòng)態(tài)連接,就是:一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,虛擬機(jī)就是根據(jù)這個(gè)信息知道要調(diào)用哪個(gè)具體的方法。

    直接引用有兩種方式來定位對(duì)象,句柄和直接指針??聪旅娴膱D加深下理解:

    虛擬機(jī)棧里面 reference 可以理解成直接引用,換句話說,直接引用 存儲(chǔ) 在虛擬機(jī)棧中(并不是說,其它地方就不能存儲(chǔ)直接引用了,因?yàn)槲乙膊恢榔渌胤侥懿荒艽鎯?chǔ)直接引用,比如 static 類型的對(duì)象的直接引用)。

    從這里也可以映證一點(diǎn):在內(nèi)存分配與回收過程中,判斷對(duì)象是否可達(dá)的可達(dá)性分析算法中:可作為GC roots 的對(duì)象有:虛擬機(jī)棧中引用的對(duì)象。

    對(duì)符號(hào)引用和直接引用有了一定認(rèn)識(shí)之后,最后來看看:符號(hào)引用是如何變成直接引用的?先來看張圖:

    類從被載到虛擬機(jī)內(nèi)存,到卸載出內(nèi)存為止,整個(gè)生命周期如上圖。那有些 符號(hào)引用轉(zhuǎn)化成直接引用,是不是也發(fā)生在上面某個(gè)階段呢?

    其實(shí)就是根據(jù) 在哪個(gè)階段 符號(hào)引用 轉(zhuǎn)化成直接引用,將方法調(diào)用分成:解析調(diào)用 與 分派調(diào)用。

    在類加載的解析階段,會(huì)將一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是:方法在程序真正運(yùn)行之前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期是不可改變的。

    換句話說,調(diào)用目標(biāo)在程序代碼寫好、編譯器進(jìn)行編譯時(shí)就必須確定下來,這類方法的調(diào)用稱為解析

    只要能被 invokestatic 和 invokespecial 指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,符合這個(gè)條件的方法有:靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類方法 4類。

    下面來看下,這四類方法 調(diào)用的字節(jié)碼指令和符號(hào)引用是啥?

    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();//靜態(tài)方法調(diào)用StaticResolution sr = new StaticResolution();sr.sayBye();//私有方法調(diào)用} }

    使用javap -v StaticResolution 對(duì)class文件反編譯,查看main方法的內(nèi)容如下:

    • 序號(hào)0 是靜態(tài)方法的調(diào)用

      這個(gè)靜態(tài)方法的描述符 是sayHello:()V,由于靜態(tài)方法是與類相關(guān)的,不能在一個(gè)類里面再定義一個(gè)與描述符sayHello:()V一樣的方法,不然編譯期就會(huì)提示“重名的方法”錯(cuò)誤。(雖然可以通過修改字節(jié)碼的方式,在同一個(gè)class字節(jié)碼文件里面可存在2個(gè)方法描述符相同的方法,但是在類加載的驗(yàn)證階段,就會(huì)驗(yàn)證失敗,具體可參考從虛擬機(jī)指令執(zhí)行的角度分析JAVA中多態(tài)的實(shí)現(xiàn)原理中提到的方法描述符與特征簽名的區(qū)別)

    “雖然可以通過修改字節(jié)碼的方式,在同一個(gè)class字節(jié)碼文件里面可存在2個(gè)方法描述符相同的方法”表明:class 字節(jié)碼的描述能力是強(qiáng)于Java語言的,這也驗(yàn)證了為什么可以將其他類型的語言(比如 動(dòng)態(tài)類型)轉(zhuǎn)換成字節(jié)碼,從而運(yùn)行在JVM上。只要class字節(jié)碼能有效地支持這種 動(dòng)態(tài)類型 即可。
    所以,虛擬機(jī)在執(zhí)行 invokestatic 這條字節(jié)碼指令的時(shí)候,能夠根據(jù)sayHello:()V方法描述符(符號(hào)引用) 來唯一確定調(diào)用的方法就是public static void sayHello() {System.out.println("hello world");}

    • 序號(hào)7 是實(shí)例方法的調(diào)用(默認(rèn)構(gòu)造函數(shù)的調(diào)用)

    • 序列12 是私有方法的調(diào)用

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

    因此,上面四類方法的調(diào)用稱為 解析調(diào)用,對(duì)于這四類方法,它們的符號(hào)引用在 解析階段 就轉(zhuǎn)成了 直接引用。另外其實(shí)可以看出,解析調(diào)用的方法接收者是唯一確定的。

    總結(jié)一下:在java語言中,重載的方法(overload),由于方法的描述符是唯一的。因此.java文件編譯成.class字節(jié)碼后,生成的方法符號(hào)引用也是唯一的,那么Code屬性表里面方法調(diào)用指令就能確定具體調(diào)用哪個(gè)方法,因而是解析調(diào)用。

    下面再來看分派調(diào)用:

    用重載和覆蓋來解釋分派調(diào)用,可參考從虛擬機(jī)指令執(zhí)行的角度分析JAVA中多態(tài)的實(shí)現(xiàn)原理 。后面的講解也以這篇參考文章中的 圖一 和 圖二 進(jìn)行說明。

    分派調(diào)用分成兩類:靜態(tài)分派和動(dòng)態(tài)分派。其中,重載屬于靜態(tài)分派、方法覆蓋屬于動(dòng)態(tài)分派。下面來解釋一下為什么?

    在分派中,涉及到一個(gè)概念:叫實(shí)際類型 和 靜態(tài)類型。比如下面的語句:

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

    等式左邊叫靜態(tài)類型,等式右邊是實(shí)際類型。比如 man 這個(gè)引用,它的靜態(tài)類型是Human,實(shí)際類型是Man;woman這個(gè)引用,靜態(tài)類型是Human,實(shí)際類型是Woman

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

    那靜態(tài)分派調(diào)用 和 動(dòng)態(tài)分派調(diào)用的區(qū)別在哪兒呢?

    • 靜態(tài)分派

      靜態(tài)分派方法的調(diào)用(方法重載)如下:

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

      man引用和woman引用的靜態(tài)類型都是Human,因此方法重載是根據(jù) 引用的靜態(tài)類型來選擇相應(yīng)的方法執(zhí)行的,也就是說:上面兩條語句中的sayHello(Human )方法的參數(shù)類型都是Human,結(jié)果就是選擇了參數(shù)類型為 Human 的 sayHello方法執(zhí)行。

    再來解釋一下是如何確實(shí)選擇哪一個(gè)sayHello方法執(zhí)行的?完整代碼是這篇文章中的StaticDispatch.java 。main方法中有一行語句:StaticDispatch sr = new StaticDispatch();,因此 main 方法的棧幀中,局部變量表中存儲(chǔ)局部變量是sr,由于棧幀中還包含了動(dòng)態(tài)連接信息,動(dòng)態(tài)連接信息是:指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用。對(duì)于這行語句sr.sayHello(man);執(zhí)行的時(shí)候,就會(huì)去字符串常量池中尋找sayHello方法的方法描述符。sayHello方法有一個(gè)名稱為man的參數(shù),這個(gè)名為man的參數(shù)是由這條語句定義的Human man = new Man();,可以看出:名為man的參數(shù)聲明的類型是Human,并且可從class字節(jié)碼文件中看出方法描述符的內(nèi)容是sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V,因此,就能根據(jù)方法描述符唯一確定調(diào)用的方法是public void sayHello(Human guy)。

    再啰嗦一句,這里分析的是方法重載(Overload)而不是方法覆蓋(Override),是通過方法描述符來唯一確定具體調(diào)用執(zhí)行哪個(gè)方法,這與下面分析的動(dòng)態(tài)分派中 通過invokevirtual 指令運(yùn)行時(shí)解析 來確定執(zhí)行哪個(gè)方法是有區(qū)別的。

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

      動(dòng)態(tài)分派方法調(diào)用(方法覆蓋)的代碼如下:

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

      由上面可知:變量man引用的動(dòng)態(tài)類型是Man,變量woman引用的動(dòng)態(tài)類型是Woman,方法的執(zhí)行是根據(jù)引用的 實(shí)際類型來選擇相應(yīng)的方法執(zhí)行的。結(jié)果就是分別選擇了 Man類的sayHello方法 和 Woman類的sayHello方法執(zhí)行。

    當(dāng)然了,靜態(tài)分派與動(dòng)態(tài)分派的具體執(zhí)行過程的差異也可以由參考文章窺出端倪。

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

    最后,書中使用QQ和_360 的示例,談到了JAVA語言的靜態(tài)分派屬于多分派類型;動(dòng)態(tài)分派屬于單分派類型。趁著前面對(duì)分派的分析,記錄一下我的理解:

    首先,它是根據(jù)宗量的個(gè)數(shù)來區(qū)分單分派與多分派的。那宗量是什么呢?宗量可理解成:引用的靜態(tài)類型、實(shí)際類型、方法的接收者。看代碼:

    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 360son.hardChoice(new QQ());//son choose qqson2.hardChoice(new QQ());//son2 choose qqson2.hardChoice(new _360());//son2 chooes 360} }

    javap -v Dispatch 反編譯出來class字節(jié)碼文件的main方法如下:

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

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

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

    而書中說:“因?yàn)檫@里參數(shù)的靜態(tài)類型、實(shí)際類型都對(duì)方法的選擇不會(huì)構(gòu)成任何影響”,其實(shí)在編譯出class字節(jié)碼文件的時(shí)候,方法的參數(shù)的類型就已經(jīng)確定了,在這個(gè)示例中都是 類QQ,那當(dāng)然不能構(gòu)成影響了,但我總覺得這種說法有點(diǎn)勉強(qiáng),導(dǎo)致費(fèi)解。

    動(dòng)態(tài)分派不僅要看方法接收者的實(shí)際類型,也是要看方法的參數(shù)類型的,只是編譯成class文件的時(shí)候方法的參數(shù)類型就已經(jīng)確定了而已。其實(shí)也不用管,只需要明白 invokevirtual 指令解析過程的大致步驟就能區(qū)分,方法在運(yùn)行時(shí)到底是調(diào)用哪個(gè)方法了。

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

    1. 找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記作C 2. 如果在類型C中找到與常量中的描述符和簡(jiǎn)單名稱都相符的方法,則進(jìn)行訪問權(quán)限校驗(yàn),如果通過則返回這個(gè)方法的直接引用,查找過程結(jié)束;如果不通過,則返回java.lang.IllegalAccessError異常。 3. 否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類進(jìn)行第2步的搜索和驗(yàn)證過程。 4. 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

    而這兩句方法調(diào)用的符號(hào)引用也是一樣的,都是: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

    但是,這兩句的執(zhí)行結(jié)果也不一樣,根據(jù)invokevirtual指令的解析過程可知:

    father.hardChoice(new _360());語句操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指的對(duì)象的實(shí)際類型是Father。

    son2.hardChoice(new _360());語句操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指的對(duì)象的實(shí)際類型是Son2。

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

    ----2018.12.8 更新-----
    當(dāng)虛擬機(jī)執(zhí)行某個(gè)方法時(shí),會(huì)為這個(gè)方法創(chuàng)建棧幀,棧幀在 虛擬機(jī)運(yùn)行進(jìn)數(shù)據(jù)區(qū) 中的 虛擬機(jī)棧 中。棧幀包含四部分內(nèi)容:局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址。局部變量表存儲(chǔ)我們?cè)谶@個(gè)方法里面定義的局部變量,比如father這個(gè)局部變量。動(dòng)態(tài)連接是:方法調(diào)用時(shí) 指向運(yùn)行時(shí)常量池中 的方法的引用(其實(shí)就是符號(hào)引用)。比如在執(zhí)行語句father.hardChoice(new _360());時(shí), invokevirtual指令的解析過程的第一步就是:根據(jù)動(dòng)態(tài)連接信息,找到變量father的實(shí)際類型,在這個(gè)實(shí)際類型對(duì)應(yīng)的類中找符合hardChoice(new _360())的 方法符號(hào)引用( invokevirtual指令的解析過程的第二、三、四步)

    總結(jié)一下:虛擬機(jī)具體在選擇哪個(gè)方法執(zhí)行時(shí):

    根據(jù)在編譯成class字節(jié)碼文件后就確定了執(zhí)行哪個(gè)方法----解析 or 分派

    根據(jù)在方法是否由字節(jié)碼指令 invokevirtual 調(diào)用----解析 or 分派(分派調(diào)用是由 invokevirtual 指令執(zhí)行的)

    根據(jù)方法接收者的靜態(tài)類型 和 實(shí)際類型 ---- 動(dòng)態(tài)分派 or 靜態(tài)分派

    根據(jù)宗量個(gè)數(shù)來 確定具體執(zhí)行哪個(gè)方法----多分派 or 單分派

    但總感覺這樣劃分有點(diǎn)絕對(duì),不太準(zhǔn)確。

    構(gòu)思了一個(gè)星期的文章,終于完成了。

    參考文章:從虛擬機(jī)指令執(zhí)行的角度分析JAVA中多態(tài)的實(shí)現(xiàn)原理

    參考書籍:《深入理解java虛擬機(jī)》

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

    轉(zhuǎn)載于:https://www.cnblogs.com/hapjin/p/9374269.html

    總結(jié)

    以上是生活随笔為你收集整理的JAVA方法调用中的解析与分派的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。