JVM 方法调用之动态分派

1. 动态分派

一个体现是重写(override)。下面的代码,运行结果很明显。

 1 public class App { 2      3     public static void main(String[] args) { 4         Super object = new Sub(); 5         object.f(); 6     } 7 } 8  9  class Super {10     public void f() {11         System.out.println("super : f()");12     }13     14     public void f(int i) {15         System.out.println("super : f(int)");16     }17 }18 19 class Sub extends Super{20     21     @Override22     public void f() {23         System.out.println("sub : f()");24     }25     26     @Override27     public void f(int i) {28         System.out.println("sub : f(int)");29     }30     31     public void f(char c) {32         System.out.println("sub : f(char)");33     }34 }

最终输出sub : f();

那么虚拟机是怎么做到动态分派的呢?

不同的虚拟机有不同的实现,最常用的是使用虚方法表(Virtual Method Table)

2. 虚方法表

对于Super和Sub类,虚方法表大致如下:(灵魂画师)

上面的灵魂画作是什么意思呢?

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同签名的方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为向子类实现版本的入口地址。

从上图主要得出几个信息:

a. 上图的大部分方法,子类Super和Sub均没有重写,那么都指向父类Object的类型数据。f()和f(int)方法,父类子类都实现了,那么两者就指向不同的实现地址。f(char)只在子类定义实现,自然指向子类的类型数据。

b. 为了程序实现上的方便,具有相同签名的方法,在父类,子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需要的入口地址。

3. 实例分析

以本文开头的代码进行分析。通过javap命令查看main方法的指令。

其中的invokevirtual指令详细调用过程是这样的:

1)指令中的#19指的是App类的常量池中第19个常量表的索引项。这个常量表(CONSTATN_Methodref_info)记录的是方法f()信息的符号引用,JVM首先根据这个符号引用找到调用方法f()的类的全限定名com.khlin.Super,这是因为变量object被声明为Super类型。

2) 在Super类型的方法表中查找方法f(),如果找到,则将方法f()在方法表中的索引项(具体值我不了解,这里将其记为index) 记录到App类的常量池中第19个常量表中(常量池解析)。因此,如果Super类型方法表中没有f(),那么即使Sub类型的方法表有该方法,也会报编译失败。

3)在调用invokevirtual指令前有一个aload_1指令,它会将开始创建中堆中的Sub对象的引用压入操作数栈。然后invokevirtual指令会根据这个Sub对象的引用首先找到堆中的Sub对象,然后进一步找到Sub对象所属类型的方法表。

4)这时,通过2)查找的index,可以定位到Sub类型方法表中的f()方法,然后通过直接地址找到该方法字节码所在的内存空间。这就是父类和子类相同签名的方法索引序号一致的用处。

4. 综合考虑:一个可能想错的例子

将本文开头的代码里的main方法稍作修改,调用其他的方法。

1 public static void main(String[] args) {2         Super object = new Sub();3         char c = ‘a‘;4         object.f(c);5     }

结果将输出sub : f(int)

明明Sub方法里有完全一样类型的f(char)方法,却调用的是f(int).

相信通过前面的学习,已经可以明白原因了。

在object.f(c)调用时,虚拟机先到Super类的方法表里,查找最为合适的方法。

Super类里没有刚好参数为char的f(char)方法,按照前面静态分派和参数类型自动转换的学习,可以知道,编译器使用了除了f(char)之外最为合适的方法f(int)。获取到索引后,通过索引到实际对象的Sub方法表里找到f(int)方法,最终执行的就是Sub类的f(int)方法。

该方法的字节码指令证明了上述的论证。

时间: 2024-07-30 03:38:48

JVM 方法调用之动态分派的相关文章

JVM 方法调用之静态分派

分派(Dispatch)可能是静态也可能是动态的,根据分派依据的宗量数可分为单分派和多分派.这两种分派方式的两两组合就构成了静态单分派,静态多分派,动态单分派,动态多分派这4种组合.本章讲静态分派. 1.静态分派 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派.静态分派的典型应用是方法重载.静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的. 那么什么是静态类型(static type)呢? 1 Super object = new Sub(); 像上面的语句,S

JVM方法调用(invokevirtual)

在java代码运行期间,方法间的调用可以说是最为频繁的了,那么这些方法间的调用在底层的虚拟机又做了什么事情呢?现在就让我们揭开那道神秘的面纱. JVM调用方法有五条指令,分别是invokestatic,invokespecial,invokevirtual,invokeinterface,invokedynamic.invokestatic用来调用静态方法:invokespecial用来调用私有方法,父类方法(super.),类构造器方法:invokeinterface调用接口方法:invoke

JVM 方法调用之解析

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还没有涉及到方法内部的具体运行过程.在程序运行时,进行方法调用是最普遍最频繁的操作,但Class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在Class文件里存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用).这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到了运行期间才能确定目标方

JVM方法调用

当我们站在JVM实现的角度去看方法调用的时候,我们自然会想到一种分类: 1.编译代码的时候就知道是哪个方法,永远不会产生歧义,例如静态方法,private方法,构造方法,super方法. 2.运行时才能确定是哪个方法,这也正是多态的实现原理. 对于第一种方法的调用,有2个字节码指令:invokestatic,invokespecial invokestatic:调用static方法(不需要通过类的实例就可以调用),这很好理解.静态方法属于整个类型,就一份,没有歧义. invokespecial:

struts2 - 动态方法调用与动态结果调用 小 demo

访问请求: http://localhost:8080/strutsdemo1/user_index.action?username=zhangsan struts.xml 文件配置: TestAction.java 文件:

浅谈动态分派和静态分派

前言 动态分派和静态分派机制是Java多态实现的原理.本文将针对这两种机制进行浅析. 静态分派 静态分派机制最典型的代码示例如下 void test() { Father father = new Son(); //静态分派 print(father); } void print(Father father) { System.out.println("this is father"); } void print(Son son) { System.out.println("

Java静态分派和动态分派

前言 动态分派和静态分派机制是Java多态实现的原理.本文将针对这两种机制进行浅析. 静态分派 静态分派机制最典型的代码示例如下 void test() { Father father = new Son(); //静态分派 print(father); } void print(Father father) { System.out.println("this is father"); } void print(Son son) { System.out.println("

Action方法调用

一.Action访问路径 Action的访问路径是由struts.xml文件中配置的Action所在包的命名空间,Action的名字和常struts.action.extension共同决定的 例如: Xml代码 <constant name="struts.action.extension" value="action, ," /> <package name="default"  namespace="/"

Jvm(66),方法调用----动态分派调用

package demo.jvm.test8? public class Demo4 { /** * 方法动态分派演示 * */ static class Human { protected void sayHello() { System.out.println("Human say hello")? }? } static class Man extends Human { protected void sayHello() { System.out.println("m