《深入理解java虚拟机》学习笔记之虚拟机字节码执行引擎

        本篇博客主要总结一下虚拟机字节码执行引擎中方法调用的相关内容。

    方法调用并不同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时不涉及方法内部的具体运行过程。那么一说到确定被调用方法的版本,很自然的就知道这部分内容与多态相关,说具体点就是方法的重载和重写。

       所有的方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可变的。也就是说,调用目标在程序代码写好、编译器编译时就必须确定下来。这类方法的调用称为解析。
     那么在平时java语言的开发中,符合编译期可知,运行期不变这两个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法都不可能通过继承或者别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。在java虚拟机里面提供了5条方法调用字节码指令,分别如下:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器方法、私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

      只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,这些方法可以称为非虚方法,其它方法就可以称为虚方法(除去final方法,Java语言规范中明确说明了final方法是一种非虚方法)。
        说完解析,接下说下分派。解析调用一定是一个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期去完成。而分派调用则可能是静态的也可能是动态的,根据分派一句的宗量数,可以分成单分派和多分派,所以分派总的来说可以分成以下4类:静态单分派、静态多分派、动态单分派、动态多分派。
        静态单分派,我们可以先看下面这段代码:

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,man");
    }
    public void sayHello(Woman guy){
        System.out.println("hello,woman");
    }
    public static void main(String[] args){
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
    //output:
    //hello,guy
    //hello,guy
}

         在Human man = new Man();这行代码中,我们将“Human”称为变量的静态类型或者叫做外观类型,后面的“Man”则称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。例如下面的代码:

//实际类型变化
Human man = new Man();
man = new Woman();
//静态类型的变化
sr.sayHello((Man)man);
sr.sayHello((Woman)man);

        在上面的那段代码中,main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象sr的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意的定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。所以可以看出静态分派的典型应用就是方法重载。静态分派发送在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在多数情况下这个重载版本并不是唯一的,往往只能确定一个更加适合的版本,这种模糊的结论在由0和1构成的计算机世界中算是比较稀罕的事情,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断。
        说完静态分派,接下来说动态分派,静态分派的典型应用是重载,那么动态分派的典型应用很自然的就会想到是不是重写呢?没错,确实是重写。还是先看段代码再说:

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();
    }
    //output:
    //man say hello
    //woman say hello
    //woman say hello
}

        显然,这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,所以说Java虚拟机是根据实际类型来分派方法执行版本的。
        接下来还要简单介绍下单分派和多分派,首先说下宗量的概念:方法的接收者与方法的参数统称为方法的宗量。这个定义最早应该来源于《Java与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。来看下面这段代码:

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());
    }
    //output:
    //father choose 360
    //son choose qq
}

        在main函数中调用了两次hardChoice()方法,这两次调用的选择结果在程序输出中已经显示的很清楚了。先看静态分派过程,这时选择目标的方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360.这次选择需要两个宗量,所以Java语言的静态分派属于多分派类型。再看动态分派过程,由于编译期就已经决定了目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数到底是什么,因为这时参数的静态类型和实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值