聊聊JVM虚方法表和方法调用
作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」
大家好,我是呼噜噜,好久没更新文章了,今天我们来填个坑,在之前的一篇文章深挖⾯向对象编程三⼤特性 --封装、继承、多态中
我们遗留了一个问题:当父类引用指向子类对象时,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种:
- invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
- invokestatic:调用静态方法。
- invokespecial: 调用私有实例方法、构造器方法;使用super关键词调用父类的实例方法、构造器;调用所实现接口的default方法
- invokevirtual:调用非私有实例方法,也就是虚方法,运行期动态查找的过程。
- 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."<init>":()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."<init>":()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虚拟机规范》
《深入理解Java虚拟机:JVM高级特性与最佳实践第3版》
全文完,感谢您的阅读,如果我的文章对你有所帮助的话,还请点个免费的赞,你的支持会激励我输出更高质量的文章,感谢!
原文镜像:聊聊JVM虚方法表和方法调用
计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」,我们下期再见!

聊聊JVM虚方法表和方法调用的更多相关文章
- JVM 字节码(二)方法表详解
JVM 字节码(二)方法表和属性表 上一节中对 ClassFile 的整体进行了五个详细的说明, 本节围绕 ClassFile 最重要的一个内容 - 方法表的 Code 属性展开 ,更多 JVM Me ...
- java方法的虚分派和方法表
java:方法的虚分派(virtual dispatch)和方法表(method table) Java方法调用的虚分派 虚分配(Virtual Dispatch) 首先从字节码中对方法的调用说起.J ...
- java方法调用之动态调用多态(重写override)的实现原理——方法表(三)
上两篇篇博文讨论了java的重载(overload)与重写(override).静态分派与动态分派.这篇博文讨论下动态分派的实现方法,即多态override的实现原理. java方法调用之重载.重写的 ...
- JVM Java字节码方法表与属性
方法表 1.methods_count method_info,前三个字段和field_info一样 2.方法的属性结构 方法中的每个属性都是一个attribut_info结构 JVM定义了部分at ...
- Android JNI 学习(四):接口方法表 & Base Api & Exception Api
本文我们来总结一下JNI 提供的功能列表及相关的函数表. 注意:请注意使用术语“必须”来描述对JNI程序员的限制.例如,当您看到某个JNI函数必须接收非NULL对象时,您有责任确保不将NULL传递给该 ...
- 《Java虚拟机原理图解》1.5、 class文件中的方法表集合--method方法在class文件中是怎样组织的
0. 前言 了解JVM虚拟机原理是每一个Java程序员修炼的必经之路.但是由于JVM虚拟机中有很多的东西讲述的比较宽泛,在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描 ...
- JVM运行时数据区--方法区
运行时数据区结构图(温习): 堆.栈.方法区的交互关系 方法区的理解 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域 方法区在JVM启动时就会被创建,并且它的实际的物理内存 ...
- 【JavaEE】Hibernate继承映射,不用多态查询只查父表的方法
几个月前,我在博问里面发了一个问题:http://q.cnblogs.com/q/64900/,但是一直没有找到好的答案,关闭问题以后才自己解决了,在这里分享一下. 首先我重复一下场景,博问里面举的动 ...
- [原创]java WEB学习笔记79:Hibernate学习之路--- 四种对象的状态,session核心方法:save()方法,persist()方法,get() 和 load() 方法,update()方法,saveOrUpdate() 方法,merge() 方法,delete() 方法,evict(),hibernate 调用存储过程,hibernate 与 触发器协同工作
本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...
- C# 读书笔记之访问虚方法、重写方法和隐藏方法
C#允许派生类中的方法与基类中方法具有相同的签名:基类中使用关键字virtual定义虚方法:然后派生类中使用关键字override来重写方法,或使用关键字new来覆盖方法(隐藏方法). 重写方法用相同 ...
随机推荐
- ORA-01093: ALTER DATABASE CLOSE only permitted with no sessions connected DG开启MRP失败
问题描述:在10.2.0.5的备库中open状态下开启实时同步,开启失败.一直卡着,只能强制停止 SQL> alter database recover managed standby dat ...
- 【树莓派】Docker安装calibre-web搭建在线书城
一.下载docker镜像 sudo docker pull johngong/calibre-web 二.创建calibre-web镜像的映射目录,存放配置文件&书籍 mkdir /home/ ...
- Rust中的Copy和Clone
1.Copy和Clone Rust中的Copy和Clonetrait都允许创建类型实例的副本.它们都提供了一种复制类型实例的方法,但它们之间存在一些重要的区别.了解这些区别有助更好地使用这两个特征. ...
- Linux下ftp常见问题总结
Linux下ftp常见问题总结 似乎拖欠了几篇文章了@_@,来公司半年了,成长了不少!从大学毕业,直到看到http://blog.csdn.net/leixiaohua1020 雷霄骅(然而天妒英才 ...
- 机器学习02-(损失函数loss、梯度下降、线性回归、评估训练、模型加载、岭回归、多项式回归)
机器学习-02 回归模型 线性回归 评估训练结果误差(metrics) 模型的保存和加载 岭回归 多项式回归 代码总结 线性回归 绘制图像,观察w0.w1.loss的变化过程 以等高线的方式绘制梯度下 ...
- 2023-04-06:拥抱Golang,优化FFmpeg音频编码器,探究encode_audio.c的内部结构。
2023-04-06:拥抱Golang,优化FFmpeg音频编码器,探究encode_audio.c的内部结构. 答案2023-04-06: 见moonfdd/ffmpeg-go库. 这段代码是一个示 ...
- 2021-01-03:java中,描述一下什么情况下,对象会从年轻代进入老年代?
福哥答案2021-01-03: 1.对象的年龄超过一定阀值,-XX:MaxTenuringThreshold 可以指定该阀值.2.动态对象年龄判定,有的垃圾回收算法,比如 G1,并不要求 age 必须 ...
- Grafana系列-统一展示-9-Jaeger数据源
系列文章 Grafana 系列文章 配置 Jaeger data source Grafana内置了对Jaeger的支持,它提供了开源的端到端分布式跟踪.本文解释了针对Jaeger数据源的配置和查询. ...
- KubeCon EU 2023 落幕,哪些技术趋势值得关注?
KubeCon+CloudNativeCon 是云原生领域的技术盛会,上个月月末,在荷兰阿姆斯特丹举办的欧洲 KubeCon+CloudNativeCon 刚刚落下帷幕,此次大会吸引了10000多名参 ...
- Simple Factory Pattern 简单工厂模式简介与 C# 示例【创建型】【设计模式来了】
〇.简介 1.什么是简单工厂模式? 一句话解释: 客户类和工厂类严格分工,客户类只需知道怎么用,处理逻辑交给工厂类. 简单工厂模式(Simple Factory Pattern)是日常开发中常用的 ...