为了解决这个问题,Java 7 引入了一条新的指令 invokedynamic。该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。 public static void startRace(java.lang.Object) 0: aload_0 // 加载一个任意对象 1: invokedynamic race // 调用赛跑方法 (理想的调用方式) 作为 invokedynamic 的准备工作,Java 7 引入了更加底层、更加灵活的方法抽象 :方法句柄(MethodHandle)。 总结与实践 今天我介绍了 invokedynamic 底层机制的基石:方法句柄。 方法句柄是一个强类型的、能够被直接执行的引用。
不过,我们并没有讲解 invokedynamic,而是深入地探讨了它所依赖的方法句柄。 invokedynamic 指令 invokedynamic 是 Java 7 引入的一条新指令,用以支持动态语言的方法调用。 在运行过程中,每一条 invokedynamic 指令将捆绑一个调用点,并且会调用该调用点所链接的方法句柄。 在第一次执行 invokedynamic 指令时,Java 虚拟机会调用该指令所对应的启动方法(BootStrap Method),来生成前面提到的调用点,并且将之绑定至该 invokedynamic Lambda 表达式到函数式接口的转换是通过 invokedynamic 指令来实现的。该 invokedynamic 指令对应的启动方法将通过 ASM 生成一个适配器类。
invokedynamic执行时,BSM先被调用并返回一个 CallSite(调用点)对象,这个对象就和 invokedynamic链接在一起。 invokedynamic指令参数invokedynamic指令参数结构如下:jvms-6.5.invokedynamic (https://docs.oracle.com/javase/specs/jvms 该参数以CONSTANT_InvokeDynamic_info结构存放在类文件的常量池结构中,invokedynamic用两个byte宽度的常量池索引号指定。 ;}对照字节码我们可知,Lambda1相关的invokedynamic指定的CONSTANT_InvokeDynamic_info序号为3,得到如下内容:期望的方法名称和描述符该invokedynamic invokedynamic指令不是业务开发者使用的。invokedynamic指令可以用来实现Lambda语法,但是它不是只能用来实现Lambda语法。
一、前言 对于 invokedynamic 指令的实现需要方法句柄作为前提知识点。可参考 Java JVM 动态方法调用之方法句柄 MethodHandle。 本文以 Lambda 表达式中运用 invokedynamic 的实现分析。 转为字节码后,关键字节码如下: { public void lambda1(); Code: stack=1, locals=2, args_size=1 0: invokedynamic 代码执行 invokedynamic 指令时,将调用常量池对应的 BootstrapMethods(引导方法) ,引导方法返回一个动态调用站点对象 CallSite,该对象绑定了要执行的方法句柄。 官方-Using the invokedynamic Instruction Java 8 的 Lambda 表达式为什么要基于 invokedynamic?
除了invokedynamic,其他调用指令的分派逻辑在JVM中是固定的,但是invokedynamic的分派逻辑是由用户设定的引导方法(BSM)决定的。 BSM会返回一个CallSite对象,这个对象会和invokedynamic链接在一起,再次执行这条invokedynamic也不会创建新的CallSite对象。 方法句柄和invokedynamic invokedynamic通过引导方法(BSM)来使用方法句柄,与invokevirtual指令不同的是invokedynamic指令不需要receiver,它会使用 可以看出在我们的字节码指令中已经出现了invokedynamic。 invokedynamic运行时 每一个invokedynamic指令都称为Dynamic Call Site(动态调用点),invokedynamic的执行大概需要两步。 1.
可见,Lambda表达式在虚拟机层面上,是通过一种名为invokedynamic字节码指令来实现的。那么invokedynamic又是何方神圣呢? invokedynamic 指令解读 invokedynamic指令是Java 7中新增的字节码调用指令,作为Java支持动态类型语言的改进之一,跟invokevirtual、invokestatic、 结合J8Sample.class字节码,并对invokedynamic指令调用过程进行跟踪分析。总结如下: ? 依据上图invokedynamic调用步骤,我们一步一步做一个分析讲解。 ; 注意,这里InvokeDynamic不是指令,代表的是Constant_InvokeDynamic_Info结构。 通过Lambda这节,我们知道Java底层是通过invokedynamic指令来实现,由于Dalvik/ART并没有支持invokedynamic指令或者对应的替代功能。
invokedynamic介绍 如果有一种函数引用、指针就好了,但JVM中并没有函数类型表示。 当JVM要第一次执行某个地方的invokedynamic指令的时候,invokedynamic必须先进行链接(linkage)。 #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function; 5: astore_1 这里再复习一下invokedynamic 说明前面执行完invokedynamic #2, 0后,在操作数栈中插入了一个类型为Function的对象。 既然lambda表达式又不需要什么动态分派(调动哪个方法是明确的), 为什么要用invokedynamic呢?
Java的lambda表达式实现上也就借助于invokedynamic命令。 字节码中每一处含有invokeDynamic指令的位置都称为动态调用点,这条指令的第一个参数不再是代表方法调用符号引用的CONSTANT_Methodref_info常亮,而是变成为JDK7新加入的CONSTANT_InvokeDynamic_info 根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。 从上述mian方法的字节码可见,有一个invokeDynamic指令,他的参数为第7项常量(第二个值为0的参数HotSpot中用不到,占位符)。 invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer; 常量池中第7项是#7 = InvokeDynamic
而invokedynamic指令就链接到这个CallSite对象来实现运行时绑定,也即invokedynamic指令在调用时,会通过这个勾子找到lambda所代表的一个functional接口对象(也即 invokedynamic指令特性 可以看到第一条指令就是代表了lambda表达式的实现指令,invokedynamic指令,这个指令是JSR-292开始应用的规范,而鉴于兼容和扩展的考虑(可以参考Oracle invokedynamic 在常量池中关联一个CONSTANT_InvokeDynamic_info结构,这个结构可以明确invokedynamic指令的一个引导方法(bootstrap method) 结合CONSTANT_InvokeDynamic_info的结构信息来看一下这个常量池表项包含的信息。 CONSTANT_InvokeDynamic_info结构如下: ? 简单解释下这个CONSTANT_InvokeDynamic_info的结构: tag: 占用一个字节(u1)的tag,也即InvokeDynamic的一个标记值,其会转化成一个字节的tag值。
#3, 0 // InvokeDynamic #0:accept:(Ljava/lang/String;)Ljava/util/function/Consumer; 在main方法的字节码中,invokedynamic是整个lambda实现的关键,不过由于该字节码在JVM中的实现逻辑非常复杂,在这里我们就不看具体代码了,只说下大致思路。 JVM在执行invokedynamic字节码时,会根据class文件中提供的各种信息,调用java.lang.invoke.LambdaMetafactory.metafactory方法来动态生成这个类 在该类生成完毕之后,invokedynamic字节码会调用其getLambda方法来创建一个TestLambda1实例,该实例创建成功,也意味着invokedynamic字节码执行完毕。 当TestLambda当Test类生成完毕后,invokedynamic字节码会调用其get$Lambda方法,生成一个TestLambda 该实例创建完毕也意味着invokedynamic字节码执行完毕
有关invokedynamic 我们知道在java7的时候加入了动态语言的支持。上面也说到了,虚拟机规范也添加了支持动态语言的三个常量类型: ? 这里还是简单的介绍一下invokedynamic吧。 invokedynamic是一个字节码指令,也就是bytecode instruction,通过动态方法的调用来实现动态语言。 argument list for an invokedynamic bootstrap is a sequence of constants. 有了invokedynamic以后,由于invokedynamic bootstrap的静态参数列表是一系列常量,也就是好多个常量。在常量池中存储复杂数据的情况就变得比较普遍。 所以就需要一个更丰富、更高级的常量类型,这样可以解决很多开发invokedynamic协议的麻烦,从而还能改善程序性能和简化编译器逻辑。 为此新增了CONSTANT_Dynamic常量类型。
8 aload_1 9 invokeinterface #10 <java/util/List.stream : ()Ljava/util/stream/Stream;> count 1 14 invokedynamic * Typically used as a bootstrap method for {@code invokedynamic} * call sites, to support * When used with {@code invokedynamic}, this is stacked * automatically When * used with {@code invokedynamic}, this is provided by * the {@code NameAndType} of the {@code InvokeDynamic} * structure and is stacked
如 50 invokedynamic #10 <test, BootstrapMethods #0> 我们可以直接在插件上,点击命令跳转到对应官方说明文档中: https://docs.oracle.com * Typically used as a bootstrap method for {@code invokedynamic} * call sites, to support * When used with {@code invokedynamic}, this is stacked * automatically When * used with {@code invokedynamic}, this is provided by * the {@code NameAndType} of the {@code InvokeDynamic} * structure and is stacked
4、invokedynamic 有时候在写一些python脚本或者js脚本的时候,会特别羡慕这些动态语言。如果把查找目标方法的决定权,从虚拟机转嫁给用户代码,我们就会有更高的自由度。 我们单独把invokedynamic抽离出来介绍,是因为它比较复杂。和反射类似,它用于一些动态的调用场景,但它和反射有着本质的不同,效率也比反射要高的多。 和上面介绍的四个指令不同,invokedynamic并没有确切的接收对象,取而代之的,是一个叫做 CallSite 的对象。 除了lambda表达式,我们还没有其他的方式来产生invokedynamic指令。 我们了解到Java7之后的invokedynamic指令,它实际上是通过方法句柄来实现的。
它并没有创建包装Lambda函数的新对象,而是使用Java 7新引进的invokeDynamic指令将此调用点动态链接到实际的Lambda函数。 invokedynamic #0:apply:()Ljava/util/function/Function; // 调用它的map()函数 invokeinterface java/util/stream /Stream.map: (Ljava/util/function/Function;)Ljava/util/stream/Stream; InvokeDynamic魔术:在Java 7中添加了此JVM 动态链接:如果查看实际的invokedynamic指令,你将看到没有实际Lambda函数的引用(称为lambda$0)。 答案在于invokedynamic的设计方式(该指令的设计非常优雅,下次我们专门写一篇文章来介绍该指令),简单来说,就在于Lambda的名称和签名,在我们的例子中有如下代码: // lambda$0函数获取一个
JDK 8中的Lambda实现:invokedynamic机制 在JDK 8中,Lambda表达式的实现基于invokedynamic指令: 编译时,Lambda表达式会生成调用invokedynamic Thread.sleep(100); // 确保线程执行完成 } } Lambda性能优化流程图 以下是Lambda在JDK 8、JDK 17和JDK 21的性能优化流程: 说明: JDK 8使用invokedynamic 总结:Lambda性能优化的关键点 版本 优化内容 优势 JDK 8 基于invokedynamic动态生成匿名类 灵活,但开销较大,元空间消耗较高。
invokedynamic:调用动态方法; 这里我们通过一个实例将这些方法调用的字节码指令逐个列出。 bolingcavalry/Action.doAction:()V 33: return public void createThread(); Code: 0: invokedynamic #13, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; 5: astore_1 6: return // InvokeDynamic #0:run:()Ljava/lang/Runnable; 5: astore_1 6: return 可见lambda表达式对应的实际上是一个 invokedynamic调用,具体的调用内容,可以用Bytecode viewer这个工具来打开Test001.class再研究,由于反编译后得到invokedynamic的操作数是#13,我们先去常量池看看
invokedynamic与Lambda表达式实现 在Java字节码指令集中,invokedynamic是一个革命性的存在。 invokedynamic的核心机制 invokedynamic指令的工作流程可以分为三个关键阶段:引导方法调用、调用点绑定和动态链接。 Lambda表达式的实现奥秘 Java 8引入的Lambda表达式正是基于invokedynamic实现的。 invokedynamic指令在Lambda表达式实现中的应用 动态调用点的实现细节 调用点(CallSite)是invokedynamic架构中的关键抽象,主要分为三种类型: • ConstantCallSite 动态语言支持的技术深化 invokedynamic指令的出现为JVM打开了动态语言支持的大门,但其潜力远未被充分挖掘。
invokespecial:调用私有实例方法; 2. invokestatic:调用静态方法; 3. invokevirtual:调用实例方法; 4. invokeinterface:调用接口方法; 5. invokedynamic invokeinterface指令来实现的; 其实t.doAction()和a.doAction()最终都是调用Test001的实例的doAction,但是t的声明是类,a的声明是接口,所以两者的调用指令是不同的; invokedynamic :调用动态方法 在main()方法中,我们声明了一个lambda() -> System.out.println(“123”),反编译的结果如下: 0: invokedynamic #13, 0 5 : astore_1 6: return 1 可见lambda表达式对应的实际上是一个invokedynamic调用,具体的调用内容,可以用Bytecode viewer这个工具来打开Test001. class再研究,由于反编译后得到invokedynamic的操作数是#13,我们先去常量池看看13对应的内容: 是个Name and type和Bootstrap method,再细看Bootstrap
invokedynamic:调用动态方法; 这里我们通过一个实例将这些方法调用的字节码指令逐个列出。 28: invokeinterface #12, 1 33: returnpublic void createThread(); Code: 0: invokedynamic :调用动态方法 在main()方法中,我们声明了一个lambda() -> System.out.println(“123”),反编译的结果如下: 0: invokedynamic #13, 0 5: astore_1 6: return 1 可见lambda表达式对应的实际上是一个invokedynamic调用,具体的调用内容,可以用Bytecode viewer这个工具来打开 Test001.class再研究,由于反编译后得到invokedynamic的操作数是#13,我们先去常量池看看13对应的内容: 是个Name and type和Bootstrap method,再细看