虚拟机字节码执行引擎2 --方法调用

方法调用

方法调用并不等同于方法中的代码被执行,而是确定被调用方法的版本(哪一个方法要被执行),而并不涉及到方法内部的具体执行过程。
由于java中,方法调用在class文件中存储的都是符号引用,而不是实际运行时方法的直接引用(即方法的入口地址),这使得java具有较强大的动态扩展能力,但是也使得方法调用变得复杂。有些方法在类加载的时候就可以唯一确定方法版本,而有些方法要等到运行期间才能确定方法的直接引用。因此,有解析和分派两种方式来分别确定方法版本。

1.解析

所有方法调用的目标方法在class文件中都是一个常量池中的符号引用。类加载阶段,对于其中一部分可以确定版本且在运行期不会发生变化(“编译期可知,运行期不可变”)的方法,会将符号引用转化成直接引用。
符合这个要求的方法有静态方法、私有方法、实例构造器、父类方法、被final修饰的方法。它们要么属于类型,要么在外部不可被访问,要么不能被覆盖,总之,都不可能有其他的版本。

从字节码指令的角度来看,jvm支持5种方法调用字节码指令

  1. invokestatic
  2. invokespecial
  3. invokevirtual
  4. invokeinterface
  5. invokedynamic
    1和2以及加上被final修饰的方法(它使用invokevirtual调用)都可以在类加载阶段完成解析。

2.分派

方法分派是多态基本性质的体现。具体而言,方法的重载(同名,但方法的参数类型或数量不同)依赖于静态分派,方法的重写(同名,参数类型相同,但方法的版本依赖于方法接收者的实际类型)依赖于动态分派。

2.1静态分派

静态分派是指依赖静态类型来确定方法执行版本的动作。典型的静态分派就是方法重载。举个例子来说


public class StaticDispatch {
 
    public void hello(Human human) {
        System.out.println("hey,human");
    }
 
     public void hello(Woman woman) {
        System.out.println("hello,lady");
    }
 
    public void hello(Man man) {
        System.out.println("hello, gentleman");
    }
 
    public static void main(String[] args) {
        StaticDispatch staticDispatch = new StaticDispatch();
        Human woman= new Woman ();
        Human man = new Man();       
        staticDispatch.hello(man);
        staticDispatch.hello(woman);
    }
}
 
abstract class Human {
}
 
class Man extends Human { 
}
 
class Woman extends Human {
}

public class DynamicDispatch {
      psvm(){
}
}
在这个例子中 ,输出都属hey,guy。 其原因是woman和man两个变量的静态类型都是Human。
Human man = new Man()
其中Human成为变量的静态类型, 而Man则是变量的实际类型。对于方法重载,根据参数的静态类型来确定方法版本。因此选择了sayHello(Human human)这个方法版本。

静态分派实际上发生在编译阶段,在完成方法调用时,参数的类型和数量已经在程序中写好了。因此,也有人将它归为解析。

2.2动态分派

再举个例子

abstract class Human {
    public abstract boolean canBearAChild();
 
    class Man extends Human { 
        public boolean canBearAChild(){return false;}
    }
 
    class Woman extends Human {
       public boolean canBearAChild(){return true;}
    }

    public static void main(String[] args) {
        Human man = new Man();
        System.out.println(man.canBearAChild());
        Human woman= new Woman();
        System.out.println(woman.canBearAChild());
    }

显然男性不能怀小孩,女性可以。
对于动态分派,虚拟机是如何确定方法版本的呢?它将局部变量表中的引用压入操作数栈(如man),将它作为方法的接收者。然后虚拟机根据这个引用去找到它所指向对象的实际类型,接下来在该类型中找到对应的方法;如果找不到,就去父类找,直到Object类都找不到的话就报异常。
因此,重写的本质就是,先在常量池中找到方法的符号引用,然后再运行时根据接收者(这个例子中的man/woman所指向的对象)的实际类型选择方法执行版本。

值得注意的是,字段是没有多态性的。即:如果子类中定义了和父类一样的字段,子类的内存中两个字段都会存在,但是子类的字段会覆盖掉父类的字段; 如果子类没有声明,那么就从父类继承下来。

虚拟机动态分派的实现原理是虚方法表。虚方法表存放着方法的实际入口地址,如果子类中没有重写,那么子类和父类该方法的地址入口是一样的;如果重写了,那么子类虚方法表的地址被替换成指向子类实现版本的方法入口地址。引用一张图:这里Son继承并重写了Father的所有方法,因此hardChioce(QQ)和hardChioce(_360)都指向了Son类型数据;Son和Father都没有重写Object类的方法,因此这些方法都指向了Object的类型。

虚方法表

最后再举个例子。

public class AddA {
    public static void main(String[] args) {
        Father guy = new Son(30);
        guy.saySomething();
        System.out.println(guy.age);
    }
}

class Father{
    int age = 60;

    public Father() {
        saySomething();
    }

    public Father(int age) {
        this.age = age;
    }

    public void saySomething(){
        System.out.println("I am the father, " + age + "years old");
    }
}

class Son extends Father{
    int age = 20; // 把这行注释掉,看看结果,think why
    //(注释掉之后age就是从Father继承下来的,不注释则子类对象中子类的age覆盖掉父类的age)。

    public Son(int age) {
        saySomething();
        this.age = age;
        saySomething();
    }

    public void saySomething(){
        System.out.println("I am the son, " + age + " years old");
    }
}

结果:

I am the son, 0 years old
I am the son, 20 years old
I am the son, 30 years old
I am the son, 30 years old
60

解释:

第1条 I am the son, 0 years old 来源于子类构造方法中隐式调用super(),在父类的构造方法中调saySomething()打印这条语句。
    执行过程是这样的: 
      1)new Son(int age)的时候,为对象分配内存初始化对象,此时age为0。
      2)执行子类有参构造方法,首先是隐式调父类的构造方法即Father()方法。
      3)Father()方法中先执行int age = 60; 然后执行saySomething()方法。此时方法的接收者是Son对象实例,因此打印出age为0.
第2条I am the son, 20 years old 源于代码中Son(int age)内所写的第一条saySomething语句。
  执行过程是:
    构造方法搜集为字段赋值的语句,隐式的在super()之后执行。此时age = 20,因此打印出son 20岁。
第3条I am the son, 30 years old不用说
第4条是回到main方法之后,多态。方法接收者的实际类型Son,打印出son 30岁
第5条输出guy.age,对象的外观类型是Father,他所见的age就是Father类中定义的 age 60
  1. 注释掉Son中的第一条int age = 20之后
class Son extends Father{
    // int age = 20; // 把这行注释掉,看看结果,think why
    //(注释掉之后age就是从Father继承下来的,不注释则子类对象中子类的age覆盖掉父类的age)。
    ...

注释完之后输出如下

I am the son, 60 years old
I am the son, 60 years old
I am the son, 30 years old
I am the son, 30 years old
30

对注释之后结果的解释

第1条 I am the son, 60 years old 来源于子类构造方法中隐式调用super(),在父类的构造方法中调saySomething()打印这条语句。
    执行过程是这样的: 
      1)new Son(int age)的时候,为对象分配内存初始化对象,此时age为0。
      2)执行子类有参构造方法,首先是隐式调父类的构造方法即Father()方法。
      3)Father()方法中先执行int age = 60; 然后执行saySomething()方法。此时方法的接收者是Son对象实例,age是从父类继承下来的,他所见的age就是60, 打印出son 60岁.(他老爸傻了,儿子跟我一样大)
第2条I am the son, 60 years old 源于代码中Son(int age)内所写的第一条saySomething语句,没什么好说的
第3条I am the son, 30 years old不用说,此时对象的age被改成了30
第4条是回到main方法之后,多态。方法接收者的实际类型Son,打印出son 30岁
第5条输出guy.age,对象的age在之前被赋了新值30, 现在儿子也成爹了,可惜他是倒着长的233
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容