JAVA虚拟机(JVM)——虚拟机字节码执行引擎(二)

方法调用

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

解析

    所有方法调用中的目标方法在Class文件里都是一个常量符号的引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。

    静态方法和私有方法这两大类,前者与类直接关联,后者在外部不可访问,它们都适合在类加载阶段进行解析。与之对应的是在Java虚拟机里提供了5条方法调用字节码指令:
    ·invokestatic:调用静态方法
    ·invokespecial:调用实例构造器init方法、私有方法和父类方法
    ·invokevirtual:调用所以的虚方法
    ·invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
    ·invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

    只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法四类(非虚方法,其他方法出final修饰的以外,都称为虚方法),它们在类加载的时候就会把符号引用解析为该方法的直接引用
//方法静态解析演示 author zzm
public class StaticResolution{

    public static void sayHello(){
        System.out.println("hello world");
    }

    public static void main(String[] args){
        StaticResolution.sayHello();
    }
}


//使用javap命令查看这段程序的字节码,会发现的确是通过invokestatic指令来调用sayHello()方法的
D:\Develop\>javap -verbose StaticResolution
public static void main(java.lang.String[]);
    Code:
     Stack=0, Locals=1, Args_size=1
     0:   invokestatic   #31;                 //Method sayHello:()V
     3:   return
    LineNumberTable:
     line 15: 0
     line 16: 3

分派

    解析调用一定是个静态过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转换为可用的直接引用。二分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派,所以一共有4种分派组合。
静态分派
首先通过一段代码演示一下什么是静态分派
//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);
    }
}

_____________________________________________________________
运行结果:

hello guy
hello guy
对于上面的代码,我们先定义两个重要的概念:
    Human man = new Man();
我们把"Human"称为变量man的静态类型或外观类型,而"Man"则称为变量man的实际类型。
main()里面的两次sayHello()方法调用,使用哪个重载版本,完全取决于传入参数的数量和数据类型。但虚拟机(编译器)重载时是通过参数的静态类型而不是实际类型作为判断依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载。
动态分派
    动态分派与重写(Override)有着很密切的关联,我们依旧用一段代码来演示动态分派。
//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();
    }

}

_______________________________________________________________
运行结果:

man say hello
woman say hello
woman say hello
调用虚方法使用invokevirtual指令,invokevirtual指令的运行时解析过程大致分为以下几步:
(1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
(2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError异常。
(3)否则,按照继承关系从下往上依次对C类的各个父类进行第二步的搜索和验证过程。
(4)如果最终没找到合适的方法,则抛出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是java语言中重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
单分派与多分派
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分成单分派和多分派两种。至今,java语言是“静态多分派,动态单分派”的。下面通过一段代码来解释一下:
//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());
    }
}
______________________________________________________
运行结果:

father choose 360
son choose QQ
我们先看编译阶段编译器的选择过程,也就是静态分派的过程。这是选择目标方法的依据有两个:一是静态类型是Father还是Son,二是方法参数是QQ还是_360,所以这是根据两个宗量进行选择,即静态多分派。

再看运行阶段虚拟机的选择,也就是动态分派过程。由于编译器已经确定参数的类型,所以此时只需要确定接收者的实际类型是Father还是Son。所以只有一个宗量作为选择依据,即动态单分派。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值