JVM 方法调用的底层实现

方法调用的字节码指令:

字节码指令作用
invokestatic调用静态方法
invokespecial调用私有实例方法、构造器及 super 关键字等
invokevirtual调用非私有实例方法,比如 publicprotected
invokeinterface和上面这条指令类似,不过作用于接口类
invokedynamic调用动态方法

非虚方法(静态绑定)

在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。

  • invokestatic静态方法
  • invokespecial私有实例方法、实例构造器和父类方法
  • 一部分被 final 修饰的 invokevirtual 方法:非私有实例方法 publicprotected

虚方法(动态绑定)

方法在运行时是可变的。很多时候,JVM 需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic 指令加上 invokespecial 指令,就属于静态绑定过程。

  • 未被被 final 修饰的invokevirtual方法
  • invokeinterface,和上面这条指令类似,不过作用于接口类

分派

静态分派(多见于方法的重载)

  • 静态分派发生在编译阶段,使用静态类型,而非实际类型
  • 确定静态分派的动作实际上不是由虚拟机来执行的
  • 依赖静态类型来定位方法执行版本的分派动作称为静态分派

动态分派(多见于方法的重写)

invokevirtual 指令的多态查找的机制:

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

invokevirtual 可以在方法调用的时候(运行时期)知道方法的符号引用转换。部分符号引用在运行期间转化为直接引用,这种转化就是动态链接(虚拟机栈中的动态链接)

虚方法表

使用虚方法表索引来替代元数据查找以提高性能。在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

Lambda 表达式

JVM 编译的时候使用 invokedynamic 实现 Lambda 表达式,invokedynamic 是使用 MethodHandle 实现的,所以 JVM 会根据你编写的 Lambda 表达式的代码,编译出一套可以去调用 MethodHandle 的字节码代码

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

BootstrapMethods 属性在 Java 1.7 以后才有,位于类文件的属性列表中,这个属性用于保存 invokedynamic 指令引用的引导方法限定符。

和上面介绍的四个指令不同,invokedynamic 并没有确切的接受对象,取而代之的,是一个叫 CallSite 的对象。

invokedynamic 指令的底层,是使用方法句柄(MethodHandle)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的 getset 方法。

句柄类型(MethodType)是我们对方法的具体描述,配合方法名称,能够定位到一类函数。访问方法句柄和调用原来的指令基本一致,但它的调用异常,包括一些权限检查,在运行时才能被发现。

MethodHandle
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(String.class);
MethodHandles methodHandle = lookup.findVirtual(o.getClass(),"methodName", methodType);
// 往往 invoke 使用比 invokeExact 要多,因为 invokeExact 如果类型不匹配,则会抛错
Object res = methodHandle,invoke(o);

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

MethodHandle.Lookup 可以通过相应的 findxxx 方法得到相应的 MethodHandle,相当于MethodHandle 的工厂方法。查找对象上的工厂方法对应于方法、构造函数和字段的所有主要用例。

findStatic 相当于得到的是一个static方法的句柄(类似于 invokestatic 的作用),findVirtual 找的是普通方法(类似于 invokevirtual 的作用)

Lambda 表达式的性能
  • 捕获与非捕获:非捕获的性能更高
    • 当 Lambda 表达式访问一个定义在 Lambda 表达式体外的非静态变量或者对象时,这个Lambda 表达式称为“捕获的”
    • 一个非捕获的 lambda 通常比捕获的更高效,非捕获的 lambda 只需要计算一次. 然后每次使用到它都会返回一个唯一的实例。而捕获的 lambda 表达式每次使用时都需要重新计算一次,而且从目前实现来看,它很像实例化一个匿名内部类的实例。
  • Lambda 最差的情况性能内部类一样
    • Lambda 语言实际上是通过方法句柄来完成的,在调用链上自然也多了一些调用步骤,那么在性能上,是否就意味着 Lambda 性能低呢?对于大部分“非捕获”的 Lambda 表达式来说,JIT 编译器的逃逸分析能够优化这部分差异,性能和传统方式无异;但对于“捕获型”的表达式来说,则需要通过方法句柄,不断地生成适配器,性能自然就低了很多(不过和便捷性相比,一丁点性能损失是可接受的)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值