0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

浅析JVM虚方法表和方法调用

jf_78858299 来源:小牛呼噜噜 作者:小牛呼噜噜 2023-03-02 09:57 次阅读

今天我们来填坑,在之前的一篇文章深挖⾯向对象编程三⼤特性 --封装、继承、多态中 我们遗留了一个问题:当父类引用指向子类对象时,JVM是如何知晓调用的是哪个子类的方法?

动态绑定和静态绑定

我们下文还是用之前文章的例子,简单修改一下:

public class ClassTest {

    static class Animal {
        public void eat(){
            System.out.println("动物吃饭!");
        }
        public void work(){
            System.out.println("动物可以帮助人类干活!");
        }
    }

    static class Cat extends Animal {
        public void eat() {
            System.out.println("吃鱼");
        }
        public void sleep() {
            System.out.println("猫会睡懒觉");
        }
    }

    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();//此处编译会报错。
    }

}

当父类引用指向子类对象时,也就是Animal cat=new Cat();这个也叫做向上转型,重写式多态。

这种多态其实是通过 动态绑定 (dynamic binding)技术来实现,是指 在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法 。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。这种多态可通过函数的重写以及向上转型来实现。

与动态绑定相对应的就是 静态绑定 ,指的是 在JVM解析时便能够直接识别目标方法的情况 。网上有些文章说,重载和静态绑定直接挂钩,这其实是不完全正确的,笔者举个极端的例子:当某个类中的重载方法被它的子类重写时,那它其实通过了动态绑定。

重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同并且参数类型也相同的方法之间的关系

需要注意的是: 本文一直在说程序在运行期间发生的事,而方法调用在静态阶段(编译) 以声明的静态类型为准 ,不管符号引用指向的是哪个实例对象。编译成字节码再进入JVM,进行类加载图片

我们回到刚刚的例子上:cat.eat();这句的结果打印:吃鱼。程序这块调用我们子类Cat定义的方法,而不是父类的同名方法。cat.work();这句的结果打印:动物可以帮助人类干活!我们上面Cat类没有定义work方法,但是却使用了父类的方法,这是不是很神奇。

其实此处调的是父类的同名方法cat.sleep();这句 编译器会提示 编译报错。表明:当我们当子类的对象作为父类的引用使用时,只能访问子类中和父类中都有的方法,而无法去访问子类中特有的方法。

虽然向上转型是安全的。但是缺点是:一旦向上转型,子类会丢失的子类的扩展方法,其实就是 子类中原本特有的方法就不能再被调用了。所以cat.sleep()这句会编译报错。

由此我们可以发现规律:当发生向上转型,去调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。如果子类没有同名方法,会再次去调父类中的该方法。这种根据对象的实际类型而不是声明类型来选择并调用方法的过程也叫做 动态分派 (Dynamic Dispatch)图片但如果直接这样去查找,会发生循环查找,效率较低,为了解决这个问题,虚方法表 就出现了,也就是动态绑定的底层原理。

虚方法表与虚方法

JVM 虚方法表 (Virtual Method Table),也称为vtable,是动态调度用来依次调用虚方法的一种表结构,是一种特殊的 索引

面向对象编程,会频繁地触发 动态分派 ,如果每次动态分配的过程都要重新在类的方法 元数据中搜索合适的目标的方法,就可能影响到执行效率,所以JVM选择了 用空间换取时间的策略来实现动态绑定, 为每个类生成一张虚方法表 ,然后直接通过虚方法表,使用索引来代替循环查找,快速定位目标方法。

类加载器与双亲委派机制一网打尽一文中,我们知道 类的生命周期一般有如下图有7个阶段,其中阶段1-5为类加载过程,验证、准备、解析统称为连接图片虚方法表会在类加载的连接阶段被创建,JVM扫描类的方法信息,识别哪些是 虚方法 ,并在虚方法表中储存其对应的 方法的相关信息以及这些 方法在虚拟机内存方法区中的入口地址 。这入口地址就是该方法的虚拟方法表的索引,JVM可以通过这个索引地址找到对应的方法。也就是说,每个类的对象都会拥有自己的虚方法表

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

非虚方法:如果方法在编译期就确定了具体的调用版本,则这个版本在运行时是不可变的,这样的方法称为非虚方法静态方法。比如私有方法,final 方法,实例构造器,父类方法都是非虚方法,除了这些以外都是虚方法

Java中发生向上转型,呈现重写式多态时,如果子类没有重写父类方法,子类并不会复制一份父类的方法到自己的虚方法表中,就会去父类的虚方法表中查找 目标方法

子类的重写的方法和父类中的同名方法在字节码层面方法索引通常来说是一样的,如果在子类找到方法eat(),其索引是0,发现不是要调用的方法后,而是要调用父类的eat(),就会直接去父类方法索引为0的地方查找,这样能进一步提高查找效率。

图片

JVM方法调用的指令

从JVM底层来了解方法调用,我们还需知晓 在JVM中和方法调用有关的指令有5种:

  1. invokeinterface :调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
  2. invokestatic :调用静态方法。
  3. invokespecial : 调用 私有实例方法 、构造器方法;使用super关键词调用父类的实例方法、构造器;调用所实现接口的default方法
  4. invokevirtual :调用 非私有实例方法 ,也就是虚方法,运行期动态查找的过程。
  5. invokedynamic : 调用动态方法,JDK7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。(Invoke dynamic method)

我们javap来反编译上文例子生成的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

我们可以发现:Java 中所有非私有实例方法调用都会被编译成 invokevirtual指令,而接口方法调用都会被编译成 invokeinterface 指令。这两种指令,均属于Java 虚拟机中的 虚方法调用 ,会进行函数的动态绑定。

invokevirtual指令在执行时,首先在运行期确定方法接收者的实际类型,并不是把常量池中方法的符号引用(在这里相当于常量池里的方法信息)解析到直接引用上就结束了,而是接着根据方法接收者的实际类型来选择方法版本,这个过程也就是Java多态的本质。

针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table,简称itable),和虚方法表类似。

另外,当我们了解invokespecial指令,invokestatic指令时,可以知晓,父类引用在调用静态方法,私有方法或是接口default方法是不会发生多态,而是直接调用声明类型的方法。

在Java 8中Lambda表达式和默认方法时,底层会生成和使用 invokedynamic ,很有意思的一个指令,本文就不详细介绍该指令了,以后有机会再讲讲。

小结

小结一下,本文主要讲解了方法调用在Java虚拟机的实现方式,以及虚方法表在 JVM 方法调用中充当了一个中介的角色,使得 JVM 能够实现多态性和动态分派。最后带大家了解一下JVM常见的方法调用的指令,Java可不仅仅只有CRUD哦


参考资料

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

《Java虚拟机规范》

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 封装
    +关注

    关注

    125

    文章

    7605

    浏览量

    142190
  • 面向对象
    +关注

    关注

    0

    文章

    64

    浏览量

    9961
  • JVM
    JVM
    +关注

    关注

    0

    文章

    155

    浏览量

    12177
收藏 人收藏

    评论

    相关推荐

    一种优雅解决MySQL驱动中引用导致GC耗时较长问题的方法

    在之前文章中写过 MySQL JDBC 驱动中的引用导致 JVM GC 耗时较长的问题,在驱动代码(mysql-connector-java 5.1.38版本)中
    的头像 发表于 12-20 09:52 726次阅读

    不能使用“断、短”分析方法的压控双向电流源电路

    压控双向电流源电路属于线性运算放大电路,由于电路使用了正反馈电路,所以不能使用“断、短”分析方法,只能采用反馈分析方法。由于压控双向电流源电路中的四个电阻要求等值,因此需要采用印制
    发表于 04-23 19:05

    Jvm的整体结构和特点

    多个子程序方法配合,程序执行时跳往子程序前,会将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,退回到原来的程序中。  本地方法栈  本地方法栈和虚拟机栈的功能类似,为JVM
    发表于 01-05 17:23

    matlab自定义函数调用方法

    matlab自定义函数调用方法 命令文件/函数文件+ 函数文件 - 多
    发表于 11-29 13:14 88次下载

    数字板焊/断路查找方法

    数字板采用多层板及贴片焊接技术,加之元器件密集,极易出现焊或断路故障,这里向大家介绍几种判断芯片或线路焊断路的快速检查方法.
    发表于 12-23 10:49 1977次阅读

    vb调用excel方法大全

    电子发烧友网站提供《vb调用excel方法大全.docx》资料免费下载
    发表于 04-14 10:27 6次下载

    Ku波段接收前端抗干扰方法浅析

    Ku波段接收前端抗干扰方法浅析,下来看看。
    发表于 07-29 19:05 6次下载

    产生焊的原因及解决方法介绍

    本文开始阐述了什么是焊以及焊的危害,其次介绍了焊产生的主要原因及分析焊的原因和步骤,最后介绍了解决焊的
    发表于 02-27 11:06 8.5w次阅读

    浅析电机轴磨损的原因及修复方法

    浅析电机轴磨损的原因及修复方法
    发表于 01-24 16:55 2次下载

    浅析快速处理导热油管腐蚀渗漏的方法

    浅析快速处理导热油管腐蚀渗漏的方法
    发表于 02-15 09:33 2次下载

    C调用matlab方法

    C调用matlab方法介绍
    发表于 07-31 10:55 0次下载

    super调用父类的构造方法

    我们分析这句话“父类对象的引用”,那说明我们使用的时候只能在子类中使用,既然是对象的引用,那么我们也可以用来调用成员属性以及成员方法,当然了,这里的 super 关键字还能够调用父类的构造方法
    的头像 发表于 10-10 16:42 787次阅读
    super<b class='flag-5'>调用</b>父类的构造<b class='flag-5'>方法</b>

    PCB焊接焊有哪些检测方法

    PCB焊接焊检测方法
    的头像 发表于 10-18 17:15 3820次阅读

    jvm内存模型和内存结构

    内存模型是指Java程序在运行时,JVM对内存空间的组织和管理方式。它包括了线程私有的部分和线程共享的部分。 线程私有部分 线程私有部分主要包含了栈(Stack)和程序计数器(Program Counter Register)。 栈是每个线程独立拥有的,用于存储方法的局部
    的头像 发表于 12-05 11:08 747次阅读

    jvm配置的mx

    JVM配置中的mx参数主要用于设置JVM的最大堆内存大小。本文将详细介绍mx参数的作用、配置方法以及如何选择合适的值。 一、mx参数的作用 在JVM中,堆内存用于存放对象实例以及相关数
    的头像 发表于 12-05 14:24 576次阅读