0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

淺析JVM虛方法表和方法調(diào)用

jf_78858299 ? 來(lái)源:小牛呼嚕嚕 ? 作者:小牛呼嚕嚕 ? 2023-03-02 09:57 ? 次閱讀

今天我們來(lái)填坑,在之前的一篇文章深挖?向?qū)ο?a href="http://srfitnesspt.com/v/tag/1315/" target="_blank">編程三?特性 --封裝、繼承、多態(tài)中 我們遺留了一個(gè)問(wèn)題:當(dāng)父類引用指向子類對(duì)象時(shí),JVM是如何知曉調(diào)用的是哪個(gè)子類的方法?

動(dòng)態(tài)綁定和靜態(tài)綁定

我們下文還是用之前文章的例子,簡(jiǎn)單修改一下:

public class ClassTest {

    static class Animal {
        public void eat(){
            System.out.println("動(dòng)物吃飯!");
        }
        public void work(){
            System.out.println("動(dòng)物可以幫助人類干活!");
        }
    }

    static class Cat extends Animal {
        public void eat() {
            System.out.println("吃魚(yú)");
        }
        public void sleep() {
            System.out.println("貓會(huì)睡懶覺(jué)");
        }
    }

    static class Dog extends Animal {
        public void eat() {
            System.out.println("吃骨頭");
        }
    }

    public static void main(String[] args) throws Exception {
        Animal cat=new Cat();
        cat.eat();
        cat.work();
       //cat.sleep();//此處編譯會(huì)報(bào)錯(cuò)。
    }

}

當(dāng)父類引用指向子類對(duì)象時(shí),也就是Animal cat=new Cat();這個(gè)也叫做向上轉(zhuǎn)型,重寫(xiě)式多態(tài)。

這種多態(tài)其實(shí)是通過(guò) 動(dòng)態(tài)綁定 (dynamic binding)技術(shù)來(lái)實(shí)現(xiàn),是指 在執(zhí)行期間判斷所引用對(duì)象的實(shí)際類型,根據(jù)其實(shí)際的類型調(diào)用其相應(yīng)的方法 。也就是說(shuō),只有程序運(yùn)行起來(lái),你才知道調(diào)用的是哪個(gè)子類的方法。這種多態(tài)可通過(guò)函數(shù)的重寫(xiě)以及向上轉(zhuǎn)型來(lái)實(shí)現(xiàn)。

與動(dòng)態(tài)綁定相對(duì)應(yīng)的就是 靜態(tài)綁定 ,指的是 在JVM解析時(shí)便能夠直接識(shí)別目標(biāo)方法的情況 。網(wǎng)上有些文章說(shuō),重載和靜態(tài)綁定直接掛鉤,這其實(shí)是不完全正確的,筆者舉個(gè)極端的例子:當(dāng)某個(gè)類中的重載方法被它的子類重寫(xiě)時(shí),那它其實(shí)通過(guò)了動(dòng)態(tài)綁定。

重載指的是方法名相同而參數(shù)類型不相同的方法之間的關(guān)系,重寫(xiě)指的是方法名相同并且參數(shù)類型也相同的方法之間的關(guān)系

需要注意的是: 本文一直在說(shuō)程序在運(yùn)行期間發(fā)生的事,而方法調(diào)用在靜態(tài)階段(編譯) 以聲明的靜態(tài)類型為準(zhǔn) ,不管符號(hào)引用指向的是哪個(gè)實(shí)例對(duì)象。編譯成字節(jié)碼再進(jìn)入JVM,進(jìn)行類加載圖片

我們回到剛剛的例子上:cat.eat();這句的結(jié)果打?。撼贼~(yú)。程序這塊調(diào)用我們子類Cat定義的方法,而不是父類的同名方法。cat.work();這句的結(jié)果打?。簞?dòng)物可以幫助人類干活!我們上面Cat類沒(méi)有定義work方法,但是卻使用了父類的方法,這是不是很神奇。

其實(shí)此處調(diào)的是父類的同名方法cat.sleep();這句 編譯器會(huì)提示 編譯報(bào)錯(cuò)。表明:當(dāng)我們當(dāng)子類的對(duì)象作為父類的引用使用時(shí),只能訪問(wèn)子類中和父類中都有的方法,而無(wú)法去訪問(wèn)子類中特有的方法。

雖然向上轉(zhuǎn)型是安全的。但是缺點(diǎn)是:一旦向上轉(zhuǎn)型,子類會(huì)丟失的子類的擴(kuò)展方法,其實(shí)就是 子類中原本特有的方法就不能再被調(diào)用了。所以cat.sleep()這句會(huì)編譯報(bào)錯(cuò)。

由此我們可以發(fā)現(xiàn)規(guī)律:當(dāng)發(fā)生向上轉(zhuǎn)型,去調(diào)用方法時(shí),首先檢查父類中是否有該方法,如果沒(méi)有,則編譯錯(cuò)誤;如果有,再去調(diào)用子類的同名方法。如果子類沒(méi)有同名方法,會(huì)再次去調(diào)父類中的該方法。這種根據(jù)對(duì)象的實(shí)際類型而不是聲明類型來(lái)選擇并調(diào)用方法的過(guò)程也叫做 動(dòng)態(tài)分派 (Dynamic Dispatch)圖片但如果直接這樣去查找,會(huì)發(fā)生循環(huán)查找,效率較低,為了解決這個(gè)問(wèn)題,虛方法表 就出現(xiàn)了,也就是動(dòng)態(tài)綁定的底層原理。

虛方法表與虛方法

JVM 虛方法表 (Virtual Method Table),也稱為vtable,是動(dòng)態(tài)調(diào)度用來(lái)依次調(diào)用虛方法的一種表結(jié)構(gòu),是一種特殊的 索引

面向?qū)ο缶幊?,?huì)頻繁地觸發(fā) 動(dòng)態(tài)分派 ,如果每次動(dòng)態(tài)分配的過(guò)程都要重新在類的方法 元數(shù)據(jù)中搜索合適的目標(biāo)的方法,就可能影響到執(zhí)行效率,所以JVM選擇了 用空間換取時(shí)間的策略來(lái)實(shí)現(xiàn)動(dòng)態(tài)綁定, 為每個(gè)類生成一張?zhí)摲椒ū?/strong> ,然后直接通過(guò)虛方法表,使用索引來(lái)代替循環(huán)查找,快速定位目標(biāo)方法。

類加載器與雙親委派機(jī)制一網(wǎng)打盡一文中,我們知道 類的生命周期一般有如下圖有7個(gè)階段,其中階段1-5為類加載過(guò)程,驗(yàn)證、準(zhǔn)備、解析統(tǒng)稱為連接圖片虛方法表會(huì)在類加載的連接階段被創(chuàng)建,JVM掃描類的方法信息,識(shí)別哪些是 虛方法 ,并在虛方法表中儲(chǔ)存其對(duì)應(yīng)的 方法的相關(guān)信息以及這些 方法在虛擬機(jī)內(nèi)存方法區(qū)中的入口地址 。這入口地址就是該方法的虛擬方法表的索引,JVM可以通過(guò)這個(gè)索引地址找到對(duì)應(yīng)的方法。也就是說(shuō),每個(gè)類的對(duì)象都會(huì)擁有自己的虛方法表

那什么是虛方法和非虛方法?

非虛方法:如果方法在編譯期就確定了具體的調(diào)用版本,則這個(gè)版本在運(yùn)行時(shí)是不可變的,這樣的方法稱為非虛方法靜態(tài)方法。比如私有方法,final 方法,實(shí)例構(gòu)造器,父類方法都是非虛方法,除了這些以外都是虛方法

當(dāng)Java中發(fā)生向上轉(zhuǎn)型,呈現(xiàn)重寫(xiě)式多態(tài)時(shí),如果子類沒(méi)有重寫(xiě)父類方法,子類并不會(huì)復(fù)制一份父類的方法到自己的虛方法表中,就會(huì)去父類的虛方法表中查找 目標(biāo)方法。

子類的重寫(xiě)的方法和父類中的同名方法在字節(jié)碼層面方法索引通常來(lái)說(shuō)是一樣的,如果在子類找到方法eat(),其索引是0,發(fā)現(xiàn)不是要調(diào)用的方法后,而是要調(diào)用父類的eat(),就會(huì)直接去父類方法索引為0的地方查找,這樣能進(jìn)一步提高查找效率。

圖片

JVM方法調(diào)用的指令

從JVM底層來(lái)了解方法調(diào)用,我們還需知曉 在JVM中和方法調(diào)用有關(guān)的指令有5種:

  1. invokeinterface :調(diào)用接口中的方法,實(shí)際上是在運(yùn)行期決定的,決定到底調(diào)用實(shí)現(xiàn)該接口的哪個(gè)對(duì)象的特定方法。
  2. invokestatic :調(diào)用靜態(tài)方法。
  3. invokespecial : 調(diào)用 私有實(shí)例方法 、構(gòu)造器方法;使用super關(guān)鍵詞調(diào)用父類的實(shí)例方法、構(gòu)造器;調(diào)用所實(shí)現(xiàn)接口的default方法
  4. invokevirtual :調(diào)用 非私有實(shí)例方法 ,也就是虛方法,運(yùn)行期動(dòng)態(tài)查找的過(guò)程。
  5. invokedynamic : 調(diào)用動(dòng)態(tài)方法,JDK7新加入的一個(gè)虛擬機(jī)指令,相比于之前的四條指令,他們的分派邏輯都是固化在JVM內(nèi)部,而invokedynamic則用于處理新的方法分派:它允許應(yīng)用級(jí)別的代碼來(lái)確定執(zhí)行哪一個(gè)方法調(diào)用,只有在調(diào)用要執(zhí)行的時(shí)候,才會(huì)進(jìn)行這種判斷,從而達(dá)到動(dòng)態(tài)語(yǔ)言的支持。(Invoke dynamic method)

我們javap來(lái)反編譯上文例子生成的class文件ClassTest.class:

public com.zj.ideaprojects.demo.test4.ClassTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object.":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/zj/ideaprojects/demo/test4/ClassTest$Cat
         3: dup
         4: invokespecial #3                  // Method com/zj/ideaprojects/demo/test4/ClassTest$Cat.":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method com/zj/ideaprojects/demo/test4/ClassTest$Animal.eat:()V
        12: aload_1
        13: invokevirtual #5                  // Method com/zj/ideaprojects/demo/test4/ClassTest$Animal.work:()V
        16: return
      LineNumberTable:
        line 30: 0
        line 31: 8
        line 32: 12
        line 34: 16
    Exceptions:
      throws java.lang.Exception

我們可以發(fā)現(xiàn):Java 中所有非私有實(shí)例方法調(diào)用都會(huì)被編譯成 invokevirtual指令,而接口方法調(diào)用都會(huì)被編譯成 invokeinterface 指令。這兩種指令,均屬于Java 虛擬機(jī)中的 虛方法調(diào)用 ,會(huì)進(jìn)行函數(shù)的動(dòng)態(tài)綁定。

invokevirtual指令在執(zhí)行時(shí),首先在運(yùn)行期確定方法接收者的實(shí)際類型,并不是把常量池中方法的符號(hào)引用(在這里相當(dāng)于常量池里的方法信息)解析到直接引用上就結(jié)束了,而是接著根據(jù)方法接收者的實(shí)際類型來(lái)選擇方法版本,這個(gè)過(guò)程也就是Java多態(tài)的本質(zhì)。

針對(duì)于invokeinterface指令來(lái)說(shuō),虛擬機(jī)會(huì)建立一個(gè)叫做接口方法表的數(shù)據(jù)結(jié)構(gòu)(interface method table,簡(jiǎn)稱itable),和虛方法表類似。

另外,當(dāng)我們了解invokespecial指令,invokestatic指令時(shí),可以知曉,父類引用在調(diào)用靜態(tài)方法,私有方法或是接口default方法是不會(huì)發(fā)生多態(tài),而是直接調(diào)用聲明類型的方法。

在Java 8中Lambda表達(dá)式和默認(rèn)方法時(shí),底層會(huì)生成和使用 invokedynamic ,很有意思的一個(gè)指令,本文就不詳細(xì)介紹該指令了,以后有機(jī)會(huì)再講講。

小結(jié)

小結(jié)一下,本文主要講解了方法調(diào)用在Java虛擬機(jī)的實(shí)現(xiàn)方式,以及虛方法表在 JVM 方法調(diào)用中充當(dāng)了一個(gè)中介的角色,使得 JVM 能夠?qū)崿F(xiàn)多態(tài)性和動(dòng)態(tài)分派。最后帶大家了解一下JVM常見(jiàn)的方法調(diào)用的指令,Java可不僅僅只有CRUD哦


參考資料

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.2

《Java虛擬機(jī)規(guī)范》

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    一種優(yōu)雅解決MySQL驅(qū)動(dòng)中引用導(dǎo)致GC耗時(shí)較長(zhǎng)問(wèn)題的方法

    在之前文章中寫(xiě)過(guò) MySQL JDBC 驅(qū)動(dòng)中的引用導(dǎo)致 JVM GC 耗時(shí)較長(zhǎng)的問(wèn)題,在驅(qū)動(dòng)代碼(mysql-connector-java 5.1.38版本)中
    的頭像 發(fā)表于 12-20 09:52 ?791次閱讀

    不能使用“斷、短”分析方法的壓控雙向電流源電路

    壓控雙向電流源電路屬于線性運(yùn)算放大電路,由于電路使用了正反饋電路,所以不能使用“斷、短”分析方法,只能采用反饋分析方法。由于壓控雙向電流源電路中的四個(gè)電阻要求等值,因此需要采用印制
    發(fā)表于 04-23 19:05

    Jvm的整體結(jié)構(gòu)和特點(diǎn)

    多個(gè)子程序方法配合,程序執(zhí)行時(shí)跳往子程序前,會(huì)將下個(gè)指令的地址存到堆棧中,直到子程序執(zhí)行完后再將地址取出,退回到原來(lái)的程序中?! ”镜?b class='flag-5'>方法棧  本地方法棧和虛擬機(jī)棧的功能類似,為JVM
    發(fā)表于 01-05 17:23

    matlab自定義函數(shù)調(diào)用方法

    matlab自定義函數(shù)調(diào)用方法 命令文件/函數(shù)文件+ 函數(shù)文件 - 多
    發(fā)表于 11-29 13:14 ?88次下載

    數(shù)字板焊/斷路查找方法

    數(shù)字板采用多層板及貼片焊接技術(shù),加之元器件密集,極易出現(xiàn)焊或斷路故障,這里向大家介紹幾種判斷芯片或線路焊斷路的快速檢查方法.
    發(fā)表于 12-23 10:49 ?1997次閱讀

    vb調(diào)用excel方法大全

    電子發(fā)燒友網(wǎng)站提供《vb調(diào)用excel方法大全.docx》資料免費(fèi)下載
    發(fā)表于 04-14 10:27 ?6次下載

    Ku波段接收前端抗干擾方法淺析

    Ku波段接收前端抗干擾方法淺析,下來(lái)看看。
    發(fā)表于 07-29 19:05 ?6次下載

    產(chǎn)生焊的原因及解決方法介紹

    本文開(kāi)始闡述了什么是焊以及焊的危害,其次介紹了焊產(chǎn)生的主要原因及分析焊的原因和步驟,最后介紹了解決焊的
    發(fā)表于 02-27 11:06 ?8.5w次閱讀

    淺析電機(jī)軸磨損的原因及修復(fù)方法

    淺析電機(jī)軸磨損的原因及修復(fù)方法
    發(fā)表于 01-24 16:55 ?2次下載

    淺析快速處理導(dǎo)熱油管腐蝕滲漏的方法

    淺析快速處理導(dǎo)熱油管腐蝕滲漏的方法
    發(fā)表于 02-15 09:33 ?2次下載

    C調(diào)用matlab方法

    C調(diào)用matlab方法介紹
    發(fā)表于 07-31 10:55 ?0次下載

    super調(diào)用父類的構(gòu)造方法

    我們分析這句話“父類對(duì)象的引用”,那說(shuō)明我們使用的時(shí)候只能在子類中使用,既然是對(duì)象的引用,那么我們也可以用來(lái)調(diào)用成員屬性以及成員方法,當(dāng)然了,這里的 super 關(guān)鍵字還能夠調(diào)用父類的構(gòu)造方法
    的頭像 發(fā)表于 10-10 16:42 ?829次閱讀
    super<b class='flag-5'>調(diào)用</b>父類的構(gòu)造<b class='flag-5'>方法</b>

    PCB焊接焊有哪些檢測(cè)方法

    PCB焊接焊檢測(cè)方法
    的頭像 發(fā)表于 10-18 17:15 ?4047次閱讀

    jvm內(nèi)存模型和內(nèi)存結(jié)構(gòu)

    內(nèi)存模型是指Java程序在運(yùn)行時(shí),JVM對(duì)內(nèi)存空間的組織和管理方式。它包括了線程私有的部分和線程共享的部分。 線程私有部分 線程私有部分主要包含了棧(Stack)和程序計(jì)數(shù)器(Program Counter Register)。 棧是每個(gè)線程獨(dú)立擁有的,用于存儲(chǔ)方法的局部
    的頭像 發(fā)表于 12-05 11:08 ?801次閱讀

    jvm配置的mx

    JVM配置中的mx參數(shù)主要用于設(shè)置JVM的最大堆內(nèi)存大小。本文將詳細(xì)介紹mx參數(shù)的作用、配置方法以及如何選擇合適的值。 一、mx參數(shù)的作用 在JVM中,堆內(nèi)存用于存放對(duì)象實(shí)例以及相關(guān)數(shù)
    的頭像 發(fā)表于 12-05 14:24 ?643次閱讀