JVM 方法调用的底层实现
方法调用的字节码指令:
字节码指令 | 作用 |
---|---|
invokestatic |
调用静态方法 |
invokespecial |
调用私有实例方法、构造器及 super 关键字等 |
invokevirtual |
调用非私有实例方法,比如 public 和 protected |
invokeinterface |
和上面这条指令类似,不过作用于接口类 |
invokedynamic |
调用动态方法 |
非虚方法(静态绑定)
在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。
invokestatic
静态方法invokespecial
私有实例方法、实例构造器和父类方法- 一部分被
final
修饰的invokevirtual
方法:非私有实例方法public
和protected
虚方法(动态绑定)
方法在运行时是可变的。很多时候,JVM 需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic
指令加上 invokespecial
指令,就属于静态绑定过程。
- 未被被
final
修饰的invokevirtual
方法 invokeinterface
,和上面这条指令类似,不过作用于接口类
分派
静态分派(多见于方法的重载)
- 静态分派发生在编译阶段,使用静态类型,而非实际类型
- 确定静态分派的动作实际上不是由虚拟机来执行的
- 依赖静态类型来定位方法执行版本的分派动作称为静态分派
动态分派(多见于方法的重写)
invokevirtual
指令的多态查找的机制:
- 找到操作数栈顶的第一个元素所指向的对象实际类型,记做 c;
- 如果在类型 c 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,查找过程结束,不通过则返回
java.lang.IllegalAccessError
- 否则,按照继承关系从下往上依次对 c 的各个父类进行第二步的搜索和验证过程;
- 如果始终没找到合适的方法,则抛出
java.lang.AbstractMethodError
异常,这就是 Java 语言中方法重写的本质。
invokevirtual
可以在方法调用的时候(运行时期)知道方法的符号引用转换。部分符号引用在运行期间转化为直接引用,这种转化就是动态链接(虚拟机栈中的动态链接)
虚方法表
使用虚方法表索引来替代元数据查找以提高性能。在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
Lambda 表达式
JVM 编译的时候使用 invokedynamic
实现 Lambda 表达式,invokedynamic
是使用 MethodHandle
实现的,所以 JVM 会根据你编写的 Lambda 表达式的代码,编译出一套可以去调用 MethodHandle
的字节码代码
invokedynamic
这个字节码是比较复杂。和反射类似,它用于一些动态的调用场景,但它和反射有着本质的不同,效率也比反射要高得多。
BootstrapMethods
属性在 Java 1.7 以后才有,位于类文件的属性列表中,这个属性用于保存 invokedynamic
指令引用的引导方法限定符。
和上面介绍的四个指令不同,invokedynamic
并没有确切的接受对象,取而代之的,是一个叫 CallSite
的对象。
invokedynamic
指令的底层,是使用方法句柄(MethodHandle
)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的 get
和 set
方法。
句柄类型(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 编译器的逃逸分析能够优化这部分差异,性能和传统方式无异;但对于“捕获型”的表达式来说,则需要通过方法句柄,不断地生成适配器,性能自然就低了很多(不过和便捷性相比,一丁点性能损失是可接受的)。