JVM的动态分派及静态分派

1、静态分派

首先我们先明确,什么是静态类型,什么是动态类型(也称为实际类型)。
静态类型就是在编译期的时候可知的,动态类型则是在程序运行期间才可以知道的。这里来看一个例子:

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,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);
        
    }

}

在这个例子中,Human man = new Man(); Human 就是属于静态类型,Man则属于动态类型。man这个对象在使用的时候,可以为Man,或者是Women,这个是未知的。但是静态类型为Human则是一开始(编译期间)就知道的。

那什么是静态分派呢,所有依赖静态类型来决定方法执行版本的分派动作都称为静态分派。静态分派的典型例子就是重载。所以上面的例子的结果是

hello,human!

hello,human!

重载不会看动态类型,所以都是根据静态类型来分派的,都是进入了Human的参数的方法。

2、动态分派

动态分派和多态的重写息息相关。继续使用上面的例子扩展一下:

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();
    }
}

如果你们学过java的多态特性的话,这里的结果没太多问题,你们都知道是什么了。

man say hello
woman say hello
woman say hello

这里很明显是动态分派了,man和woman都根据了他们的动态类型去选择方法去执行。那这里就有一个问题出现了,man在中间变化了两次类型,那java是如何根据实际类型来分派方法的呢。我们尝试使用javap来寻找一下答案。

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class JvmDemo/DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method JvmDemo/DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class JvmDemo/DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method JvmDemo/DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method JvmDemo/DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method JvmDemo/DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class JvmDemo/DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method JvmDemo/DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method JvmDemo/DynamicDispatch$Human.sayHello:()V
        36: return

0到15都是属于Human man = new Man(); Human woman = new Woman();这两句代码,意思是创建man和woman这两个对象,并且放到局部变量表中。16和20行分别把局部变量表的第一个变量和第二个对象的引用压到栈顶,这两个对象是将要执行sayHello()方法的所有者。17和21行是方法调用的指令,我们可以看到,这两条指令无论从哪里来看,都是一模一样的。这样看来,问题的原因就出现在了invokevirtual指令本身。

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

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

根据这个规范,我们可以知道,invokevirtual指令会根据方法的接受者的实际类型去动态的选择方法版本,这个过程就是方法重写的本质,也就是动态分派。

既然动态分派是跟invokevirtual指令的执行有关,那我们就可以得出一个结论,动态分派跟字段无关,只和方法有关。**事实上,在java里,只有虚方法存在,字段永远都不会是虚的。换句话说就是,字段不参与多态。**当哪个类的方法访问某个字段的时候,该字段指的就是这个类能看到的字段。当父类和子类的字段同名的时候,子类会屏蔽父类的同名字段。

下面再来看一个例子,也就是所谓的面试题:

public class FieldHasNoPolymorphic {
    static class Father {
        public int money = 1;
        public Father() {
            money = 2;
            showMeTheMoney();
        }
        public void showMeTheMoney() {
            System.out.println("I am Father, i have $" + money);
        }
    }
    static class Son extends Father {
        public int money = 3;
        public Son() {
            money = 4;
            showMeTheMoney();
        }
        public void showMeTheMoney() {
            System.out.println("I am Son, i have $" + money);
        }
    }
    public static void main(String[] args) {
        Father gay = new Son();
        System.out.println("This gay has $" + gay.money);
    }
}

结果是:

I am Son, i have $0
I am Son, i have $4
This gay has $2

接下来解析下为什么会这样子输出,两句I am Son是因为隐式调用了一次父类的构造方法,父类的构造器方法里面调用了一次虚方法showMeTheMoney()。实际上执行的版本是Son::showMeTheMoney()方法,因为栈顶是Son对象,所以去选择了Son的虚方法,所以输出是I am Son。第一个输出I am Son的时候,虽然money字段在父类中已经初始化化为2了,但是子类的money还没有被初始化,所以此时的money为0(super方法是第一个执行的)。第二个son的输出相信大家都没有什么问题。第三个money是通过静态类型访问到了父类的money。顺便把javap的指令贴一下:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class JvmDemo/FieldHasNoPolymorphic$Son
         3: dup
         4: invokespecial #3                  // Method JvmDemo/FieldHasNoPolymorphic$Son."<init>":()V
         7: astore_1
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: new           #5                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        18: ldc           #7                  // String This gay has $
        20: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        23: aload_1
        24: getfield      #9                  // Field JvmDemo/FieldHasNoPolymorphic$Father.money:I
        27: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        30: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        33: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        36: return

参考资料:
《深入理解Java虚拟机》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值