JAVA方法调用中的解析与分派
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, guyman引用和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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WMI介绍和应用查询硬件信息(硬盘信息、
- 下一篇: Xray使用的一些经验分享(xray+b