写在前面
本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和文献引用请见100个问题搞定Java虚拟机
解答
invokedynamic 是 Java 7 引入的一条新指令,用以支持动态语言的方法调用。
具体来说,它将调用点(CallSite)抽象成一个 Java 类,并且将原本由 Java 虚拟机控制的方法调用以及方法链接暴露给了应用程序。
在运行过程中,每一条 invokedynamic 指令将捆绑一个调用点,并且会调用该调用点所链接的方法句柄。
补充
方法句柄
方法句柄是 invokedynamic 底层机制的基石。 它是一个强类型的、能够被直接执行的引用。
该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。
唯一标识
方法句柄的类型(MethodType)是由所指向方法的参数类型以及返回类型组成的。它是用来确认方法句柄是否适配的唯一标识。
当使用方法句柄时,我们其实并不关心方法句柄所指向方法的类名或者方法名。
创建
方法句柄的创建是通过 MethodHandles.Lookup 类来完成的。
它提供了多个 API,既可以使用反射 API 中的 Method 来查找,也可以根据类、方法名以及方法句柄类型来查找。
调用方法句柄,和原本对应的调用指令是一致的。
也就是说,对于原本用 invokevirtual 调用的方法句柄,它也会采用动态绑定;而对于原本用 invokespecial 调用的方法句柄,它会采用静态绑定。
权限检查
方法句柄的权限检查发生在创建过程中,相较于反射调用节省了调用时反复权限检查的开销。
操作
方法句柄还支持增删改参数的操作,这些操作是通过生成另一个充当适配器的方法句柄来实现的。
方法句柄 VS 反射 VS 代理
访问控制方面
- 反射需要调用setAccesible(),可能会受到安全管理器的禁止警告;
- 代理有些情况下通过内部类实现,但是内部类只能访问受限的函数或字段;
- 方法句柄则在上下文中对所有方法都有完整的访问权限,并且不会受到安全管理器的限制,这是方法句柄的优势之一。
执行速度方面
- 反射的性能会受到参数方法、类型的自动装箱和拆箱、方法内联的影响,相对来讲反射算是执行较慢的了
- 通过代理的方式因调用Java函数实现,速度与其它调用函数的速度是一样的,相对较快;
- 方法句柄可能不会有代理方式那样的执行速度快,但同样会受到JVM等不同的配置导致速度不同,但从JVM设计者的角度来说,应该是力求达到像调用函数一样快的速度,目前可能是达不到的。方法句柄的调用和反射调用一样,都是间接调用,同样会面临无法内联的问题。
内存开销方面
代理通常声明多个类,需要占用方法区,而方法句柄并不需要像代理一样有多个类的开销,不需要方法区的开销。
invokedynamic 的实现原理
在第一次执行 invokedynamic 指令时,Java 虚拟机会调用该指令所对应的启动方法(BootStrap Method),来生成调用点,并且将之绑定至该 invokedynamic 指令中。
在之后的运行过程中,Java 虚拟机则会直接调用绑定的调用点所链接的方法句柄。
启动方法
在字节码中,启动方法是用方法句柄来指定的。这个方法句柄指向一个返回类型为调用点的静态方法。
该方法必须接收三个固定的参数
- Lookup 类实例
- 一个用来指代目标方法名字的字符串
- 该调用点能够链接的方法句柄的类型
除了这三个必需参数之外,启动方法还可以接收若干个其他的参数,用来辅助生成调用点,或者定位所要链接的目标方法。