jvm探秘十一:虚拟机执行子系统之方法调用

本文详细介绍了Java中方法调用的过程,包括方法调用与执行的区别、符号引用与直接引用的概念,以及方法调用如何在类加载期间确定目标方法。此外,还深入探讨了静态分派与动态分派的原理,以及它们在Java中的具体应用。

概述


方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,方法调用是最普遍和频繁的操作。

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用)。Java方法调用需要在类加载期间,甚至是运行期间才能确定目标方法的直接引用。

一:解析


所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用。
在类加载的解析阶段会将一部分符号引用转化为直接引用,这里的直接引用对于类变量、类方法等来说是指向方法区的内存指针,对于类实例和实例变量等则是存储的偏移量。它有一个前提,方法在调用之前必须有一个可确定的调用版本,并且这个版本在运行期间不会改变。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
在java语言中满足“编译期确定,运行期不变”的方法有静态方法和私有方法两大类。静态方法直接和类绑定,私有方法不能被外部访问。这两种适合在类加载阶段进行解析。

Java虚拟机中提供了5条方法调用字节码指令:

  • invokestatic:调用静态方法
  • invokespecial:调用类实例的构造器方法,私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic:分派逻辑由用户所设定的引导方法决定。

非虚方法:只要能被invokestatic和invokespecial指令调用的方法,都可以在类加载的时候把符号引用解析为该方法的直接引用。这里主要是指,私有方法,静态方法,实例构造器,父类方法.(Java中明确说明了final方法是一种非虚方法,虽然被invokevirtual调用,但它无法被覆盖,没有其它版本)

虚方法(除去final方法),被invokevirtual和invokeinterface调用的则为虚方法,因为在编译期间并不能确定要调用的真正方法,所以称为虚方法。。

解析调用一定静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把符号引用转为直接引用

二:分派


众所周知java面向对象的三个重要特性,封装、继承、多态。而在jvm层面多态的实现由分派完成。分派有静态分派、动态分派。

静态分派

方法静态分派演示,静态分派的典型“重载”。

/**
 * 方法静态分派演示
 * @author yuli
 *
 */
public class StaticDispatch {
     static abstract class Human {}  

        static class Man extends Human {}  

        static class Woman extends Human {}  

        public void sayHello(Human human) {  
            System.out.println("hello,human");  
        }  

        public void sayHello(Man man) {  
            System.out.println("hello,man");  
        }  

        public void sayHello(Woman woman) {  
            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);  
        }  
}

运行结果

hello human
hello human

程序运行的时候public void sayHello(Human human) 这个方法被调用了。

Human man = new Man();

代码里的Human称为静态类型(或者叫外观类型):其变化仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的。

而Man称为实际类型:其变化的结果在运行期才可确定,编译器不编译程序时并不知道一个对象的实际类型是什么。

代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,所以结果会是这样子。

//实际类型变化  
Human man = new Man();  
man = new Woman();  

//静态类型变化  
sr.sayHello((Man)man);  
sr.sayHello((Woman)man);  

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派典型的应用是方法重载,.静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的 。对于方法参数的匹配也是根据变量的静态类型来确定,在很多情况下根据参数的类型并不能找到唯一的方法调用,这个时候的处理方式是找到一个最合适的方法。比如:

public class OverLoad {  
    public static void sayHello(char arg) {  
        System.out.println("hello char");  
    }  
    public static void sayHello(int arg) {  
        System.out.println("hello int");  
    }  
    public static void sayHello(long arg) {  
        System.out.println("hello long");  
    }  
    public static void sayHello(Character arg) {  
        System.out.println("hello Character");  
    }  
    public static void sayHello(Serializable arg) {  
        System.out.println("hello Serializable");  
    }  
    public static void sayHello(Object arg) {  
        System.out.println("hello object");  
    }  
    public static void sayHello(char ...arg) {  
        System.out.println("hello arg...");  
    }  

    public static void main(String[] args) {  
        sayHello('a');  
    }  
}  

从头注解方法,结果会按顺序输出。
基本类型是重载按char->int->long->float->double顺序匹配的。
可变参数的重载优先级是最低的。

动态分派

动态分派的例子,静态分派的典型“重写”。

/**
 * 
 * @author yuli
 *
 */
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

显然这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量在两次调用中执行了不同的方法。

导致这个现象的原因:是这两个变量的实际类型不同。

invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。这种在运行期间根据实际类型确定方法执行版本的过程称为动态分派。

动态分派的一个重要体现就是方法的重写,虽然父类引用可以指向子类对象,但是动态分派的方法调用是在运行时根据对象的实际类型去确认的

单分派和多分派

方法的接收者和方法的参数统称为宗量。可以根据宗量将分派划分为单分派和多分派。单分派是根据一个宗量对对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

/** 
 * 单分派,多分派演示 
 */  
public class Dispatch {  
    static class QQ{}  
    static class _360{}  

    public static class Father {  
        public void hardChoose(QQ args) {  
            System.out.println("父亲选择 qq");  
        }  
        public void hardChoose (_360 args) {  
            System.out.println("父亲选择 360");  
        }  
    }  

    public static class Son extends Father {  
        public void hardChoose(QQ args) {  
            System.out.println("儿子选择 qq");  
        }  
        public void hardChoose (_360 args) {  
            System.out.println("儿子选择 360");  
        }  
    }  

    public static void main(String[] args) {  
        Father father = new Father();  
        Father son = new Son();  
        father.hardChoose(new _360());  
        son.hardChoose(new QQ());  
    }

运行结果:

父亲选择 360
儿子选择 qq

看编译阶段编译器的选择过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。产生了两条invokevirtual指令,分别指向常量池中的Father.hardChoice(360)和Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。

再看运行阶段虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoose(new QQ())”这句代码时(准确的说是在执行这句代码所对应的invokevirtual指令),由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机不会关心传递过来的参数”QQ”是什么,因为这时参数的静态类型、实际类型都对方的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。
Java语言是一门静态多分派,动态单分派的语言。

虚拟机动态分派的实现

由于动态分派是非常频繁的,而且动态分派的方法版本选择过程需要在运行时在类的方法元数据中搜索合适的目标方法。虚拟机在实际实现动态分派基于性能的考虑,会在jvm在实现层面提供了一个叫做虚方法表的索引来代替元数据查找以提高性能。虚方法表结构图:
这里写图片描述
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果重写了这个方法,子类方法表中的地址将会替换指向子类实现版本的入口地址。

Father是父类son是子类,并且子类重写了父类的连个方法,hardChoice(QQ),hardChoice(_360),因此子类中的这两个方法指向了Son的类型数据,而这两个类都继承自Object且没重写它的任何方法,因此都指向了Object的类型数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值