JVM方法分派:静态多分派、动态单分派

本文介绍了JVM中的方法分派,包括静态分派和动态分派的概念。静态分派依赖静态类型决定方法执行版本,如重载;动态分派在运行时确定接收者实际类型,如虚方法调用。Java中,静态方法和非虚方法在编译阶段即可确定调用,而虚方法在运行时通过虚方法表动态查找。单分派和多分派基于方法宗量数量进行区分。

本文主要参考:《深入理解Java虚拟机 jvm的高级特性和最佳实践》  以及  博文:http://rednaxelafx.iteye.com/blog/652719


博客先从一个简单的问题开始。


Q1:什么是静态方法,什么事实例方法?他们的区别?

     众所周知的是:静态方法是用关键词static修饰的方法,而与之对应,没有static修饰的方法称之为实例方法。

                       区别 ①:“静态方法”的调用总是不指定某个对象实例为“接收者”,而“实例方法”则总是要以某个对象实例为“接收者”(receiver)。所以两者的调用形式为:  类.方法名 和对象.方法名。(Ps:类不是一个对象,所以他不是接受者)

                                ②:两者在JVM上的区别:在调用类方法时,所有参数按顺序存放于被调用方法的局部变量区中的连续区域,从局部变量0开始;在调用实例方法时,局部变量0用于存放传入的该方法所属的对象实例(Java语言中的“this”),所有参数从局部变量1开始存放在局部变量区的连续区域中。简单点来说,静态的方法的参数里面没有this,而实例方面里面会隐式的传入this.

                       注意:有时候会看到  对象.静态方法,在这里起效果等于  该对象类.静态方法。 这里调用变量的类型上声明的静态方法的语法糖而已。


Q2:是什么虚方法,什么是非虚方法? 什么时候进行静态绑定? 什么时候进行运行时绑定?

           非虚方法:指的是用static、private、final关键词修饰的词。这样划分的原因是:这三种修饰词修饰的方法在子类无法被覆写(覆写并不是指重新写了一个完全一样的方法,而且写了之后不会覆盖父类原本的方法)。

           虚方法:除开非虚方法之外的都是虚方法,也就是说,Java里只有非private的成员方法是虚方法。

           根据非虚方法划分的原因可以知道,调用目标总是固定的一个,所以编译器就可以确定唯一的方法,且在运行期它不会变化。.因此可以在编译期间对此方法进行绑定,否则需要进行动态绑定。


Q3:在JVM调用虚方法和非虚方法时本质区别是什么?

            在JVM规定了五条方法调用字节码调用字节码指令:       

invokestatic:调用静态方法
invokeespecail:调用父类方法、私有方法、实例构造器
invokevirtual:调用所有虚方法
invokeinterface:调用接口方法,会运行是确定一个调用次方法的对象
invokedynamic:先运行时动态解析出调用点限定符所引用的方法,然后执行该方法。
             其中:jvm在使用invokestatic、incokeespecail两个指令码调用的方法都是非虚方法,使用invokevirtual调用的是虚方法。(final例外)


Q4:什么是静态分配?什么是动态分派?各自的实例。

           静态分派:所有依赖静态类型,而不是实例类型来定位方法执行版本的分派动作称为静态分派。

          

    第一个例子:《深入理解Java虚拟机》里面的经典例子:
    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("human say hello");  
        }  
          
        public void sayHello(Man man){  
            System.out.println("man say hello");  
        }  
          
        public void sayHello(Woman woman){  
            System.out.println("woman say hello");  
        }  
          
        /** 
         * @param args 
         */  
        public static void main(String[] args) {  
            Human man = new Man();  
            Human woman = new Woman();  
            StaticDispatch sd = new StaticDispatch();  
            sd.sayHello(man);  //输出human say hello
            sd.sayHello(woman);  //输出human say hello
        }  
      
    }  
根据书上说:类似 Human man = new Man();   Human被称为变量的静态类型(注意,这里是变量的实际类型,和Human这个class是不是staitc无关。) , Man称之为变量的实际类型。两种类型的区别是:变量的静态类型只有在使用时才发生变化,并且它本身的静态类型不会被改变,而且在编译器可知;实际类型变化结果在运行期才可知。比如


//实际类型变化
Human man = new Human;
man = new Women()
man的实际类型在不同时刻是不同的,上面为human,下来问women
//静态类型变化
sr.sayHello((Man)man);
这里man的静态类型始终为human

继续看第一个例子:又开始的定义可以知道,这里的函数sayHello是虚方法,方法接受者为sr;现在到底确定用那个函数是根据传入的参数的数量和类型决定的。JVM在重载是通过参数的静态类型而不是实际类型判断调用依据。并且静态类型是编译器可知。 由此可知在编译阶段,javac编译器就已经决定根据参数的静态类型来调用哪个重载版本。如果是一个非虚方法呢?非虚方法不可覆写,具有唯一性。因此在编译阶段也可以完全确定下来。只有虚方法才有分派一说。附上字节码部分。划红线的可以看到,调用invokevirtual,的确是调用虚方法。



           动态分派:运行期确定接受者实际类型。

 <pre name="code" class="java">第一个例子:《深入理解Java虚拟机》里面的经典例子:
   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");  
            }  
              
        }  
      
        /** 
         * @param args 
         */  
        public static void main(String[] args) {  
            Human man = new Man();  
            Human woman = new Woman();  
            man.sayHello();                    //输出 man say hello
            woman.sayHello();                  //输出  women say hello
        }  
      
    }  


      invokevirtual的详细过程:

      1.找到操作数栈的第一元素所指向的实际类型,称为c

      2.在c中找到描述符和简单名都相同的方法,进行访问权限校验。通过返回方法的引用,不通过返回IllegalAccessError;

      3.未找到按继承关系从下到上对各个父类进行第二步操作。

     4未找到,返回异常。

    多态发生的真正步骤在第一、二步,根据传入的接受者不同,从而调用的函数不同。

Q4:什么是单分派,什么是多分派。

    首先了解一个概念:宗量。

     宗量:方法的接受者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派。

   

    public class Dispatcher {  
      
        static class QQ {  
        }  
      
        static class _360 {  
        }  
      
        public static class Father {  
            public void hardChoice(QQ qq) {  
                System.out.println("father choose qq");  
            }  
      
            public void hardChoice(_360 _360) {  
                System.out.println("father choose 360");  
            }  
        }  
          
        public static class Son extends Father{  
            public void hardChoice(QQ qq) {  
                System.out.println("son choose qq");  
            }  
      
            public void hardChoice(_360 _360) {  
                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());  
              
        }  
      
    }  

根据上述已经的讲的只是分析一下:

       1.首先father.hardChoice(new _360()),使用invokevirtual字节码调用。

        对于:

     Father father = new Father();  <pre name="code" class="java">     father.hardChoice(new _360());  


        对于编译器的选择过程,选择目标方法有两点依据: 1.this传入的类型   2.方法参数 。这里需要两个宗量进行选择,因此静态多分派。

 

      对于:

      Father son = new Son();  <pre name="code" class="java">      son.hardChoice(new QQ());  


    由于编译器已经决定目标方法的签名必须是hardchode(QQ),这是vm不会关心传过来的参数;唯一可以影响选择的因素是this的实际类型。只有一个宗量作为选择依据。因此动态单分派。

附:

★:虚拟机动态分派的实现:

    类在方法区中建议一个虚方法表,虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的。都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类的实现入口。

    

★:final且非private修饰的语句用invokevirtual调用的原因。

  参考:http://hllvm.group.iteye.com/group/topic/27064?page=2

关键词:分离编译,二进制兼容性

A.java
Java代码  收藏代码

    public class A {  
      public void foo() { /* ... */ }  
    }  



B.java
Java代码  收藏代码

    public class B extends A {  
      public void foo() { /* ... */ }  
    }  



C.java
Java代码  收藏代码

    public class C extends B {  
      public final void foo() { /* ... */ }  
    }  



这样的话有3个源码文件,它们可以分别编译。三个类有继承关系,每个都有自己的foo()的实现。其中C.foo()是final的。

那么如果在别的什么地方,
Java代码  收藏代码

    A a = getA();  
    a.foo();  


这个a.foo()应该使用invokevirtual是很直观的对吧?
而这个实际的调用目标也有可能是C.foo(),对吧?

所以为了设计的简单性,以及更好的二进制兼容性……(此处省略 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值