方法调用的底层实现

方法调用的字节码指令

关于方法的调用,Java 字节码 共提供了 5 个指令,来调用不同类型的方法:
invokestatic 用来调用静态方法;
invokespecial 用于调用私有实例方法、构造器及 super 关键字等;
invokevirtual 用于调用非私有实例方法,比如 public 和 protected,大多数方法调用属于这一种;
invokeinterface 和上面这条指令类似,不过作用于接口类;
invokedynamic 用于调用动态方法。

以上字节码可以通过代码实现再用jclasslib插件查看

非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为 非虚方法。只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java 语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法 4 种,再加上被 final 修饰的方法(尽管它使用 invokevirtual 指令调用),这 5 种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。不需要在运行时再去完成。
 invokestatic 用来调用静态方法;
在这里插入图片描述
在这里插入图片描述
invokespecial 用于调用私有实例方法、构造器及 super 关键字等;
在这里插入图片描述

虚方法

与非虚方法相反,不是虚方法的方法就是 虚方法。主要包括以下字节码中的两类
invokevirtual 用于调用非私有实例方法,比如 public 和 protected,大多数方法调用属于这一种(排除掉被 final 修饰的方法);
invokeinterface 和上面这条指令类似,不过作用于接口类;
为什么叫做虚方法呢?就是方法在运行时 是可变的。
很多时候,JVM 需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic 指令加上 invokespecial 指令,就属于静态绑定过程。因为 invokeinterface 指令跟 invokevirtual 类似,只是作用与接口,所以我们只要熟悉 invokevirtual 即可。

分派

java 是一门面向对象的程序语言,因为 Java 具备面向对象的 3 个基本特征:继承、封装和多态。
分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在 Java 虚拟机之中是如何实现的

静态分派。

多见于方法的重载。( 重载:一个类中允许同时存在一个以上的同名方法,这些方法的参数个数或者类型不同)

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 sr = new StaticDispatch();
//		sr.sayHello(man);
//		sr.sayHello(woman);
		StaticDispatch sr = new StaticDispatch();
		//实际类型变化
		Human human=new Man();
		//静态类型变化
		sr.sayHello((Man)human);
		human=new Woman();
		sr.sayHello((Woman)human);


	}
}

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

代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human)作为调用目标。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
总结:例子很简单,方法会根据你送入的参数有不同的表现形式,这个就是分派。

动态分派

多见于方法的重写。
重写:在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改方法的访问权限,修改返回类型的为父类返回类型的子类。

package ex8;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

public class DynamicDispatchNew {
    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{
        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();
    }
}

在这里插入图片描述
重写也是使用 invokevirtual 指令,只是这个时候具备多态性。
invokevirtual 指令有多态查找的机制,该指令运行时,解析过程如下:找到操作数栈顶的第一个元素所指向的对象实际类型,记做 c;如果在类型 c 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,查找过程结束,不通过则返回 java.lang.IllegalAccessError;否则,按照继承关系从下往上依次对 c 的各个父类进行第二步的搜索和验证过程;如果始终没找到合适的方法,则抛出 java.lang.AbstractMethodError 异常,这就是 Java 语言中方法重写的本质。invokevirtual 可以知道方法 call()的符号引用转换是在运行时期完成的,在方法调用的时候。部分符号引用在运行期间转化为直接引用,这种转化就是 动态链接

方法表

动态分派会执行非常频繁的动作,JVM 运行时会频繁的、反复的去搜索元数据,所以 JVM 使用了一种优化手段,这个就是在 方法区中建立一个虚方法表。使用虚方法表索引来替代元数据查找以提高性能。在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。下图中,Son 重写了来自 Father 的全部方法,因此 Son 的方法表没有指向 Father 类型数据的箭头。但是 Son 和 Father都没有重写来自 Object 的方法,所以它们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。
在这里插入图片描述

接口调用

invokeinterface 和 invokevirtual 指令类似,不过作用于接口类;

Lambda 表达式

invokedynamic 这个字节码是比较复杂。和反射类似,它用于一些动态的调用场景,但它和反射有着本质的不同,效率也比反射要高得多。

在这里插入图片描述
在这里插入图片描述
invokedynamic
BootstrapMethods 属性在 Java 1.7 以后才有,位于类文件的属性列表中,这个属性用于保存 invokedynamic 指令引用的引导方法限定符。和上面介绍的四个指令不同,invokedynamic 并没有确切的接受对象,取而代之的,是一个叫 CallSite 的对象。

方法句柄(MethodHandle)

官方文档解释:https://docs.oracle.com/javase/7/docs/api/java/lang/invoke/MethodHandles.html
invokedynamic 指令的底层,是使用方法句柄(MethodHandle)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的 get 和 set 方法,从以下案例中可以看到 MethodHandle 提供的一些方法。

在这里插入图片描述
MethodHandle 是什么?简单的说就是方法句柄,通过这个句柄可以调用相应的方法。
用 用 MethodHandle 调用方法的流程为:
(1) 创建 MethodType,获取指定方法的签名(出参和入参)
(2) 在 Lookup 中查找 MethodType 的方法句柄 MethodHandle
(3) 传入方法参数通过 MethodHandle 调用方法

MethodType
MethodType 表示一个方法类型的对象,每个 MethodHandle 都有一个 MethodType 实例,MethodType 用来指明方法的返回类型和参数类型。其有多个工厂方法的重载。

Lookup
MethodHandle.Lookup 可以通过相应的 findxxx 方法得到相应的 MethodHandle,相当于 MethodHandle 的工厂方法。查找对象上的工厂方法对应于方法、构造函数和字段的所有主要用例。findStatic 相当于得到的是一个 static 方法的句柄(类似于 invokestatic 的作用), findVirtual 找的是普通方法(类似于 invokevirtual 的作用)
参考以下代码理解,类似于反射

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
 *
 * 方法句柄(MethodHandle)使用案例
 **/
public class MethodHandleDemo {
    static  class  Bike  {
        String  sound()  {
            return  "ding ";
        }
    }
    static  class  Animal  {
        String  sound()  {
            return  "wow ";
        }
    }

    static  class  Man  extends  Animal  {
        @Override
        String  sound()  {
            return  "ha ha ha";
        }
    }

    String  sound(Object  o)  throws  Throwable  {
            //方法句柄
            MethodHandles.Lookup  lookup  =  MethodHandles.lookup();
            MethodType  methodType  =  MethodType.methodType(String.class);
            MethodHandle  methodHandle  =  lookup.findVirtual(o.getClass(),  "sound",  methodType);
            String  obj  =  (String)  methodHandle.invoke(o);
            return  obj;
    }

    public  static  void  main(String[]  args)  throws  Throwable {
        String str = new MethodHandleDemo().sound(new Bike());
        System.out.println(str);
        str = new MethodHandleDemo().sound(new Animal());
        System.out.println(str);
        str = new MethodHandleDemo().sound(new Man());
        System.out.println(str);
    }
}

对比下MethodHandle和Reflection
1.Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的3个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual(以及invokeinterface)和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
·Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等的运行期信息。而
后者仅包含执行该方法的相关信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级

·由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还在继续完善中),而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。
MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已,而且Java在这里并不是主角。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值