首页  

JVM里的方法调用分派     所属分类 jvm 浏览量 933
方法调用并不等同于方法中的代码被执行,
方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),
暂时还未涉及方法内部的具体运行过程。

解析
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,
在类加载的解析阶段,会将符合“编译期可知,运行期不可变”的方法符号引用转化为直接引用。
换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。
这类方法的调用被称为解析(Resolution)。

静态方法、私有方法、实例构造器、父类方法4种,
再加上被final修饰的方法(尽管它使用invokevirtual指令调用),
这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。
这些方法统称为“非虚方法”(Non-VirtualMethod),
与之相反,其他方法就被称为“虚方法”(Virtual Method)

分派Dispatch
1.静态分派
英文一般是“Method Overload Resolution”,所以其实是个动态概念

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 staticDispatch = new StaticDispatch();
        staticDispatch.sayHello(man);
        staticDispatch.sayHello(woman);
    }
}
运行结果
Hello guy
Hello guy

Human hu = new Man():



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


所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。
静态分派的最典型应用表现就是方法重载。
静态分派发生在编译阶段,
因此确定静态分派的动作实际上不是由虚拟机来执行的,
这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。


动态分派
Java语言多态性的另外一个重要体现——重写(Override)。

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!


根据《Java虚拟机规范》,invokevirtual指令的运行时解析过程大致分为以下几步:

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



正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,
所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,
还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。
这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

这种多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,
只会对方法有效,对字段是无效的,因为字段不使用这条指令。
事实上,在Java里面只有虚方法存在,字段永远不可能是虚的,
字段永远不参与多态,哪个类的方法访问某个名字的字段时,
该名字指的就是这个类能看到的那个字段。
当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,
但是子类的字段会遮蔽父类的同名字段。

上一篇     下一篇
JVM模块化系统

Mac快速锁屏

JVM虚拟机执行引擎

JVM动态语言支持

深入理解java虚拟机笔记

classpath资源读取问题