JVM(十四:方法調(diào)用)

這里的方法調(diào)用并不等同于方法中的代碼被執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),暫時(shí)還未涉及方法內(nèi)部的具體運(yùn)行過(guò)程。

由于Class文件的編譯過(guò)程中不包含傳統(tǒng)程序語(yǔ)言編譯的連接步驟,一切方法調(diào)用在Class文件里面存儲(chǔ)的都只是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(也就是之前說(shuō)的直接引用)。這個(gè)特性給Java帶來(lái)了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但也使得Java方法調(diào)用過(guò)程變得相對(duì)復(fù)雜,某些調(diào)用需要在類(lèi)加載期間,甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用。

方法調(diào)用字節(jié)碼指令
調(diào)用不同類(lèi)型的方法,字節(jié)碼指令集里設(shè)計(jì)了不同的指令。在Java虛擬機(jī)支持以下5條方法調(diào)用字節(jié)碼指令,分別是:
1,·invokestatic。用于調(diào)用靜態(tài)方法。
2,·invokespecial。用于調(diào)用實(shí)例構(gòu)造器<init>()方法、私有方法和父類(lèi)中的方法。
3,·invokevirtual。用于調(diào)用所有的虛方法。
4,·invokeinterface。用于調(diào)用接口方法,會(huì)在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)該接口的對(duì)象。
5,·invokedynamic。先在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,然后再執(zhí)行該方法。前面4條調(diào)用指令,分派邏輯都固化在Java虛擬機(jī)內(nèi)部,而invokedynamic指令的分派邏輯是由用戶(hù)設(shè)定的引導(dǎo)方法來(lái)決定的。

解析

所有方法調(diào)用的目標(biāo)方法在Class文件里面都是一個(gè)常量池中的符號(hào)引用,在類(lèi)加載的解析階段,會(huì)將其中的一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能夠成立的前提是:方法在程序真正運(yùn)行之前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期是不可改變的。換句話說(shuō),調(diào)用目標(biāo)在程序代碼寫(xiě)好、編譯器進(jìn)行編譯那一刻就已經(jīng)確定下來(lái)。這類(lèi)方法的調(diào)用被稱(chēng)為解析(Resolution)。

解析調(diào)用一定是個(gè)靜態(tài)的過(guò)程,在編譯期間就完全確定,在類(lèi)加載的解析階段就會(huì)把涉及的符號(hào)引用全部轉(zhuǎn)變?yōu)槊鞔_的直接引用,不必延遲到運(yùn)行期再去完成。

在Java語(yǔ)言中符合“編譯期可知,運(yùn)行期不可變”這個(gè)要求的方法,主要有靜態(tài)方法和私有方法兩大類(lèi),前者與類(lèi)型直接關(guān)聯(lián),后者在外部不可被訪問(wèn),這兩種方法各自的特點(diǎn)決定了它們都不可能通過(guò)繼承或別的方式重寫(xiě)出其他版本,因此它們都適合在類(lèi)加載階段進(jìn)行解析。

虛方法與非虛方法

只要能被invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,Java語(yǔ)言里符合這個(gè)條件的方法共有靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類(lèi)方法4種,再加上被final修飾的方法(盡管它使用invokevirtual指令調(diào)用),這5種方法調(diào)用會(huì)在類(lèi)加載的時(shí)候就可以把符號(hào)引用解析為該方法的直接引用。這些方法統(tǒng)稱(chēng)為“非虛方法”(Non-Virtual Method),與之相反,其他方法就被稱(chēng)為“虛方法”(Virtual Method)。

使用Jclasslib打開(kāi)編譯生成的class文件。選擇show方法,查看字節(jié)碼指令。
靜態(tài)方法都是使用invokestatic調(diào)用。
私有方法是使用invokespecial調(diào)用。
父類(lèi)的普通方法是通過(guò)invokespecial調(diào)用。
父類(lèi)的final方法是通過(guò)invokevirtual調(diào)用。

分派

另一種主要的方法調(diào)用形式:分派(Dispatch)調(diào)用則要復(fù)雜許多,它可能是靜態(tài)的也可能是動(dòng)態(tài)的,按照分派依據(jù)的宗量數(shù)可分為單分派和多分派。這兩類(lèi)分派方式兩兩組合就構(gòu)成了靜態(tài)單分派、靜態(tài)多分派、動(dòng)態(tài)單分派、動(dòng)態(tài)多分派4種分派組合情況。

靜態(tài)分派-重載

    /**
     * 方法靜態(tài)分派演示
     * @author zzm
     */
    public class StaticDispatch {
        static abstract class Human {
        }
        static class Man extends Human {
        }
        static class Woman extends Human {
        }
        public void sayHello(Human guy) {
            System.out.println("hello,guy!");
        }
        public void sayHello(Man guy) {
            System.out.println("hello,gentleman!");
        }
        public void sayHello(Woman guy) {
            System.out.println("hello,lady!");
        }
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            StaticDispatch sr = new StaticDispatch();
            sr.sayHello(man);
            sr.sayHello(woman);
        }
    }

運(yùn)行結(jié)果:

hello,guy!
hello,guy!

我們把上面代碼中的“Human”稱(chēng)為變量的“靜態(tài)類(lèi)型”(Static Type),或者叫“外觀類(lèi)型”(Apparent Type),后面的“Man”則被稱(chēng)為變量的“實(shí)際類(lèi)型”(Actual Type)或者叫“運(yùn)行時(shí)類(lèi)型”(Runtime Type)。靜態(tài)類(lèi)型和實(shí)際類(lèi)型在程序中都可能會(huì)發(fā)生變化,區(qū)別是靜態(tài)類(lèi)型的變化僅僅在使用時(shí)發(fā)生,變量本身的靜態(tài)類(lèi)型不會(huì)被改變,并且最終的靜態(tài)類(lèi)型是在編譯期可知的;而實(shí)際類(lèi)型變化的結(jié)果在運(yùn)行期才可確定,編譯器在編譯程序的時(shí)候并不知道一個(gè)對(duì)象的實(shí)際類(lèi)型是什么。

// 實(shí)際類(lèi)型變化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
// 靜態(tài)類(lèi)型變化
sr.sayHello((Man) human)
sr.sayHello((Woman) human)

對(duì)象human的實(shí)際類(lèi)型是可變的,編譯期間它完全是個(gè)“薛定諤的人”,到底是Man還是Woman,必須等到程序運(yùn)行到這行的時(shí)候才能確定。而human的靜態(tài)類(lèi)型是Human,也可以在使用時(shí)(如sayHello()方法中的強(qiáng)制轉(zhuǎn)型)臨時(shí)改變這個(gè)類(lèi)型,但這個(gè)改變?nèi)允窃诰幾g期是可知的,兩次sayHello()方法的調(diào)用,在編譯期完全可以明確轉(zhuǎn)型的是Man還是Woman。

所有依賴(lài)靜態(tài)類(lèi)型來(lái)決定方法執(zhí)行版本的分派動(dòng)作,都稱(chēng)為靜態(tài)分派。靜態(tài)分派的最典型應(yīng)用表現(xiàn)就是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動(dòng)作實(shí)際上不是由虛擬機(jī)來(lái)執(zhí)行的,這點(diǎn)也是為何一些資料選擇把它歸入“解析”而不是“分派”的原因。

動(dòng)態(tài)分派-重寫(xiě)

    /**
     * 方法動(dòng)態(tài)分派演示
     * @author zzm
     */
    public class DynamicDispatch {
        static abstract class Human {
            protected abstract void sayHello();
        }
        static class Man extends Human {
            @Override
            protected void sayHello() {
                System.out.println("man say hello");
            }
        }
        static class Woman extends Human {
            @Override
            protected void sayHello() {
                System.out.println("woman say hello");
            }
        }
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            man.sayHello();
            woman.sayHello();
            man = new Woman();
            man.sayHello();
        }
    }

運(yùn)行結(jié)果:

man say hello
woman say hello
woman say hello

這里選擇調(diào)用的方法版本是不可能再根據(jù)靜態(tài)類(lèi)型來(lái)決定的,因?yàn)殪o態(tài)類(lèi)型同樣都是Human的兩個(gè)變量man和woman在調(diào)用sayHello()方法時(shí)產(chǎn)生了不同的行為,甚至變量man在兩次調(diào)用中還執(zhí)行了兩個(gè)不同的方法。導(dǎo)致這個(gè)現(xiàn)象的原因很明顯,是因?yàn)檫@兩個(gè)變量的實(shí)際類(lèi)型不同,Java虛擬機(jī)是如何根據(jù)實(shí)際類(lèi)型來(lái)分派方法執(zhí)行版本的呢?

invokevirtual指令在運(yùn)行期確定接收者的實(shí)際類(lèi)型,會(huì)根據(jù)方法接收者的實(shí)際類(lèi)型來(lái)選擇方法版本,這個(gè)過(guò)程就是Java語(yǔ)言中方法重寫(xiě)的本質(zhì)。我們把這種在運(yùn)行期根據(jù)實(shí)際類(lèi)型確定方法執(zhí)行版本的分派過(guò)程稱(chēng)為動(dòng)態(tài)分派。
既然這種多態(tài)性的根源在于虛方法調(diào)用指令invokevirtual的執(zhí)行邏輯,那自然我們得出的結(jié)論就只會(huì)對(duì)方法有效,對(duì)字段是無(wú)效的,因?yàn)樽侄尾皇褂眠@條指令。事實(shí)上,在Java里面只有虛方法存在,字段永遠(yuǎn)不可能是虛的。

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

    /**
     * 字段不參與多態(tài)
     * @author zzm
     */
    public class FieldHasNoPolymorphic {
        static class Father {
            public int money = 1;
            public Father() {
                money = 2;
                showMeTheMoney();
            }
            public void showMeTheMoney() {
                System.out.println("I am Father, i have $" + money);
            }
        }
        static class Son extends Father {
            public int money = 3;
            public Son() {
                money = 4;
                showMeTheMoney();
            }
            public void showMeTheMoney() {
                System.out.println("I am Son, i have $" + money);
            }
        }
        public static void main(String[] args) {
            Father gay = new Son();
            System.out.println("This gay has $" + gay.money);
        }
    }

運(yùn)行結(jié)果:

I am Son, i have $0
I am Son, i have $4
This gay has $2

輸出兩句都是“I am Son”,這是因?yàn)镾on類(lèi)在創(chuàng)建的時(shí)候,首先隱式調(diào)用了Father的構(gòu)造函數(shù),而Father構(gòu)造函數(shù)中對(duì)showMeTheMoney()的調(diào)用是一次虛方法調(diào)用,實(shí)際執(zhí)行的版本是Son::showMeTheMoney()方法,所以輸出的是“I am Son”(操作數(shù)棧入棧的是Son)。而這時(shí)候雖然父類(lèi)的money字段已經(jīng)被初始化成2了,但Son::showMeTheMoney()方法中訪問(wèn)的卻是子類(lèi)的money字段,這時(shí)候結(jié)果自然還是0,因?yàn)樗阶宇?lèi)的構(gòu)造函數(shù)執(zhí)行時(shí)才會(huì)被初始化。main()的最后一句通過(guò)靜態(tài)類(lèi)型訪問(wèn)到了父類(lèi)中的money,輸出了2。

單分派與多分派
單分派是根據(jù)一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇,多分派則是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。

    /**
     * 單分派、多分派演示
     * @author zzm
     */
    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 choose 360");
            }
        }
        public static void main(String[] args) {
            Father father = new Father();
            Father son = new Son();
            father.hardChoice(new _360());
            son.hardChoice(new QQ());
        }
    }

在main()里調(diào)用了兩次hardChoice()方法,這兩次hardChoice()方法的選擇結(jié)果在程序輸出中已經(jīng)顯示得很清楚了。我們關(guān)注的首先是編譯階段中編譯器的選擇過(guò)程,也就是靜態(tài)分派的過(guò)程。這時(shí)候選擇目標(biāo)方法的依據(jù)有兩點(diǎn):一是靜態(tài)類(lèi)型是Father還是Son,二是方法參數(shù)是QQ還是360。這次選擇結(jié)果的最終產(chǎn)物是產(chǎn)生了兩條invokevirtual指令,兩條指令的參數(shù)分別為常量池中指向Father::hardChoice(360)及Father::hardChoice(QQ)方法的符號(hào)引用。因?yàn)槭歉鶕?jù)兩個(gè)宗量進(jìn)行選擇,所以Java語(yǔ)言的靜態(tài)分派屬于多分派類(lèi)型。

再看看運(yùn)行階段中虛擬機(jī)的選擇,也就是動(dòng)態(tài)分派的過(guò)程。在執(zhí)行“son.hardChoice(new QQ())”這行代碼時(shí),更準(zhǔn)確地說(shuō),是在執(zhí)行這行代碼所對(duì)應(yīng)的invokevirtual指令時(shí),由于編譯期已經(jīng)決定目標(biāo)方法的簽名必須為hardChoice(QQ),虛擬機(jī)此時(shí)不會(huì)關(guān)心傳遞過(guò)來(lái)的參數(shù)“QQ”到底是“騰訊QQ”還是“奇瑞QQ”,因?yàn)檫@時(shí)候參數(shù)的靜態(tài)類(lèi)型、實(shí)際類(lèi)型都對(duì)方法的選擇不會(huì)構(gòu)成任何影響,唯一可以影響虛擬機(jī)選擇的因素只有該方法的接受者的實(shí)際類(lèi)型是Father還是Son。因?yàn)橹挥幸粋€(gè)宗量作為選擇依據(jù),所以Java語(yǔ)言的動(dòng)態(tài)分派屬于單分派類(lèi)型。

根據(jù)上述論證的結(jié)果,我們可以總結(jié)一句:如今的Java語(yǔ)言是一門(mén)靜態(tài)多分派、動(dòng)態(tài)單分派的語(yǔ)言。

虛擬機(jī)動(dòng)態(tài)分派的實(shí)現(xiàn)
動(dòng)態(tài)分派是執(zhí)行非常頻繁的動(dòng)作,而且動(dòng)態(tài)分派的方法版本選擇過(guò)程需要運(yùn)行時(shí)在接收者類(lèi)型的方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此,Java虛擬機(jī)實(shí)現(xiàn)基于執(zhí)行性能的考慮,真正運(yùn)行時(shí)一般不會(huì)如此頻繁地去反復(fù)搜索類(lèi)型元數(shù)據(jù)。面對(duì)這種情況,一種基礎(chǔ)而且常見(jiàn)的優(yōu)化手段是為類(lèi)型在方法區(qū)中建立一個(gè)虛方法表。


虛方法表中存放著各個(gè)方法的實(shí)際入口地址。如果某個(gè)方法在子類(lèi)中沒(méi)有被重寫(xiě),那子類(lèi)的虛方法表中的地址入口和父類(lèi)相同方法的地址入口是一致的,都指向父類(lèi)的實(shí)現(xiàn)入口。如果子類(lèi)中重寫(xiě)了這個(gè)方法,子類(lèi)虛方法表中的地址也會(huì)被替換為指向子類(lèi)實(shí)現(xiàn)版本的入口地址。圖中,Son重寫(xiě)了來(lái)自Father的全部方法,因此Son的方法表沒(méi)有指向Father類(lèi)型數(shù)據(jù)的箭頭。但是Son和Father都沒(méi)有重寫(xiě)來(lái)自O(shè)bject的方法,所以它們的方法表中所有從Object繼承來(lái)的方法都指向了Object的數(shù)據(jù)類(lèi)型。
為了程序?qū)崿F(xiàn)方便,具有相同簽名的方法,在父類(lèi)、子類(lèi)的虛方法表中都應(yīng)當(dāng)具有一樣的索引序號(hào),這樣當(dāng)類(lèi)型變換時(shí),僅需要變更查找的虛方法表,就可以從不同的虛方法表中按索引轉(zhuǎn)換出所需的入口地址。虛方法表一般在類(lèi)加載的連接階段進(jìn)行初始化,準(zhǔn)備了類(lèi)的變量初始值后,虛擬機(jī)會(huì)把該類(lèi)的虛方法表也一同初始化完畢。
摘抄:《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》-第八章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容