inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中。 indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。所谓应用级的代码其实是一个方法,在这里这个方法被称为引导方法(Bootstrap Method),简称 BSM。BSM 返回一个 CallSite(调用点) 对象,这个对象就和 inDy 链接在一起了。以后再执行这条 inDy 指令都不会创建新的 CallSite 对象。CallSite 就是一个 MethodHandle(方法句柄)的 holder。方法句柄指向一个调用点真正执行的方法。

理解 MethodHandle(方法句柄)的一种方式就是将其视为以安全、现代的方式来实现反射的核心功能。

一个 java 方法的实体有四个构成:

  1. 方法名
  2. 签名--参数列表和返回值
  3. 定义方法的类
  4. 方法体(代码)

同一个类中,方法名相同,签名不同,JVM 会视为不同的方法,不过在 Java 中只支持签名的参数列表部分,也就是重载多态。一次方法调用,除了要方法的实体外,还要调用者(caller)和接收者(receiver),调用者也就是方法调用语句所在的类。接收者是一个对象,每个方法调用都要一个接收者,它可以是隐藏的(this),也可以是类方法,比如: String.valueOf,类也是 Class 的一个实例。

MethodType 表示方法签名。

用 MethodHandle 实现的方法调用的示例如下,可以看到方法的四个构成:

Object rcvr = "a";
try {MethodType mt = MethodType.methodType(int.class); // 方法签名MethodHandles.Lookup l = MethodHandles.lookup(); // 调用者,也就是当前类。调用者决定有没有权限能访问到方法MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt); //分别是定义方法的类,方法名,签名int ret;try {ret = (int)mh.invoke(rcvr); // 代码,第一个参数就是接收者System.out.println(ret);} catch (Throwable t) {t.printStackTrace();}
} catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) {e.printStackTrace();
} catch (IllegalAccessException x) {x.printStackTrace();
}复制代码

详细可参考:

  • Invokedynamic - Java’s Secret Weapon
  • 和译文 Invokedynamic:Java的秘密武器 - 知乎专栏

java8 lambda 表达式

lambda 表达式 是怎么使用 inDy 呢?以一段简单的代码为例

public class LambdaTest {public static void main(String[] args) {Runnable r = () -> System.out.println(Arrays.toString(args));r.run();}
}复制代码

javap -v -p LambdaTest 查看字节码,可以发现寥寥几行 java 代码生成的字节码却不少,单单常量池常量就有 66 个之多。输出见 LambdaTest.class。

可以发现多出了一个新方法,方法体就是 lambda 体(lambda body),转换为源码如下:

private static void lambda$main$0(java.lang.String[] args){System.out.println(Arrays.toString(args));
}复制代码

主要看一下 main 方法,并没有直接调用上面的方法,而是出现一条 inDy 指令:

public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=2, args_size=10: aload_01: invokedynamic #2,  0              // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;6: astore_17: aload_18: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V13: return复制代码

可以看到 inDy 指向一个类型为 CONSTANT_InvokeDynamic_info 的常量项 #2,另外 0 是预留参数,暂时没有作用。

#2 = InvokeDynamic      #0:#30         // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;复制代码

#0 表示在 Bootstrap methods 表中的索引:

BootstrapMethods:0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;Method arguments:#28 ()V#29 invokestatic com/company/LambdaTest.lambda$main$0:([Ljava/lang/String;)V#28 ()V复制代码

#30 则是一个 CONSTANT_NameAndType_info,表示方法名和方法类型(返回值和参数列表),这个会作为参数传递给 BSM。

#30 = NameAndType        #43:#44        // run:([Ljava/lang/String;)Ljava/lang/Runnable;复制代码

再看回表中的第 0 项,#27 是一个 CONSTANT_MethodHandle_info,实际上是个 MethodHandle(方法句柄)对象,这个句柄指向的就是 BSM 方法。在这里就是:

java.lang.invoke.LambdaMetafactory.metafactory(MethodHandles.Lookup,String,MethodType,MethodType,MethodHandle,MethodType)复制代码

BSM 前三个参数是固定的,后面还可以附加任意数量的参数,但是参数的类型是有限制的,参数类型只能是

  • String
  • Class
  • int
  • long
  • float
  • double
  • MethodHandle
  • MethodType

LambdaMetafactory.metafactory 带多三个参数,这些的参数的值由 Bootstrap methods 表 提供:

Method arguments:#25 ()V#26 invokestatic com/company/LambdaTest.lambda$main$0:()V#25 ()V复制代码

inDy 所需要的数据大概就是这些,可参考 Java8学习笔记(2) -- InvokeDynamic指令 - CSDN博客

inDy 运行时

每一个 inDy 指令都称为 Dynamic Call Site(动态调用点),根据 jvm 规范所说的,inDy 可以分为两步,这两步部分代码代码是在 java 层的,给 metafactory 方法设断点可以看到一些行为。

第一步 inDy 需要一个 CallSite(调用点对象),CallSite 是由 BSM 返回的,所以这一步就是调用 BSM 方法。代码可参考:java.lang.invoke.CallSite#makeSite

调用 BSM 方法可以看作 invokevirtual 指令执行一个 invoke 方法,方法签名如下:

invoke:(MethodHandle,Lookup,String,MethodType,/*其他附加静态参数*/)CallSite复制代码

前四个参数是固定的,被依次压入操作栈里

  1. MethodHandle,实际上这个方法句柄就是指向 BSM
  2. Lookup, 也就是调用者,是 Indy 指令所在的类的上下文,可以通过 Lookup#lookupClass()获取这个类
  3. name ,lambda 所实现的方法名,也就是"run"
  4. invokedType,调用点的方法签名,这里是 methodType(Runnable.class,String[].class)

接下来就是附加参数,这些参数是灵活的,由Bootstrap methods 表提供,这里分别是:

  1. samMethodType,其实就是 Runnable.run 的描述符: methodType(void.class)。sam 就 single public abstract method 的缩写
  2. implMethod: 编译器给生成的 desugar 方法,是一个 MethodHandle:caller.findStatic(LambdaTest.class,"lambda$main$0",methodType(void.class))
  3. instantiatedMethodType: Runnable.run 运行时的描述符,如果方法泛型的,那这个类型可能不一样。这里是 methodType(void.class)

上面说的固定其实应该是指 inDy 传递的实参类型是固定的,BSM 形参声明可以是随意,保证 BSM 能被调用就行,比如说 Lookup 声明为 Object 不影响调用。

接下来就是执行 LambdaMetafactory.metafactory 方法了,它会创建一个匿名类,这个类是通过 ASM 编织字节码在内存中生成的,然后直接通过 unsafe 直接加载而不会写到文件里。不过可以通过下面的虚拟机参数让它运行的时候输出到文件

-Djdk.internal.lambda.dumpProxyClasses=<path>复制代码

这个类是根据 lambda 的特点生成的,输出后可以看到,在这个例子中是这样的:

import java.lang.invoke.LambdaForm.Hidden;// $FF: synthetic class
final class LambdaTest$$Lambda$1 implements Runnable {private final String[] arg$1;private LambdaTest$$Lambda$1(String[] var1) {this.arg$1 = var1;}private static Runnable get$Lambda(String[] var0) {return new LambdaTest$$Lambda$1(var0);}@Hiddenpublic void run() {LambdaTest.lambda$main$0(this.arg$1);}
}复制代码

然后就是创建一个 CallSite,绑定一个 MethodHandle,指向的方法其实就是生成的类中的静态方法 LambdaTest$$Lambda$1.get$Lambda(String[])Runnable。然后把调用点对象返回,到这里 BSM 方法执行完毕。

更详细的可参考:

  • 浅谈Lambda Expression - 简书
  • [Java] 关于OpenJDK对Java 8 lambda表达式的运行时实现的查看方式 - 知乎专栏

第二步,就是执行这个方法句柄了,这个过程就像 invokevirtual 指令执行 MethodHandle#invokeExact 一样,

加上 inDy 上面那一条 aload_0 指令,这是操作数栈有两个分别是:

  1. args[],lambda 里面调用了 main 方法的参数
  2. 调用点对象(CallSite),实际上是方法句柄。如果是 CostantCallSite 的时候,inDy 会直接跟他的方法句柄链接。见代码:MethodHandleNatives.java#L255

传入 args,执行方法,返回一个 Runnable 对象,压入栈顶。到这里 inDy 就执行完毕。

接下来的指令就很好理解,astore_1 把栈顶的 Runnable 对象放到局部变量表的槽位1,也是变量 r。剩下的就是再拿出来调用 run 方法。

Groovy

接下来看一下 groovy 是如何使用 inDy 指令的。先复习一遍 groovy 的方法派发。

每当 Groovy 调用一个方法时,它不会直接调用它,而是要求一个中间层来代替它。 中间层通过钩子方法允许我们更改方法调用的行为。这个中间层就是 MOP(meta object proctol),MOP 主要承载的类就是 MetaClass 。一个简化版的 MOP 主要有这些方法:

  • invokeMethod(String methodName, Object args)
  • methodMissing(String name, Object arguments)
  • getProperty(String propertyName)
  • setProperty(String propertyName, Object newValue)
  • propertyMissing(String name)

可以大致认为在 Groovy 中的每个方法和属性访问调用都会转化上面的方法调用。而这些方法可以在运行时通过重写修改它的默认行为,MOP 作为方法派发的中心枢纽为 Groovy 提供了非常灵活的动态编程的能力。

现在来看一下一段简短的 groovy 代码,

class Test{int a = 0;static void main(args){Test wtf = new Test()wtf.awtf.doSomething()}
}复制代码

通过 groovyc -indy Test.groovy 把它编译成字节码。 indy 选项的意思就是启用 invokedynamic 支持。

看一下编译后的 main 方法。

public static void main(java.lang.String...);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGSCode:stack=1, locals=2, args_size=10: ldc           #2                  // class Test2: invokedynamic #44,  0             // InvokeDynamic #0:init:(Ljava/lang/Class;)Ljava/lang/Object;7: invokedynamic #50,  0             // InvokeDynamic #1:cast:(Ljava/lang/Object;)LTest;12: astore_113: aload_114: pop15: aload_116: invokedynamic #56,  0             // InvokeDynamic #2:getProperty:(LTest;)Ljava/lang/Object;21: pop22: aload_123: invokedynamic #61,  0             // InvokeDynamic #3:invoke:(LTest;)Ljava/lang/Object;28: pop29: return复制代码

可以看到一共有 4 条 inDy 指令,包括构造函数,访问成员变量,和不存在的方法调用都是 通过 invokedynamic 实现的。

再看一下引导方法表

BootstrapMethods:0: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;Method arguments:#39 <init>#40 01: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;Method arguments:#46 ()#40 02: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;Method arguments:#51 a#52 43: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;Method arguments:#58 doSomething#40 0复制代码

可以发现所有 inDy 指令的引导方法都是 IndyInterface.bootstrap

以方法调用的 inDy 指令为例,它的方法名称是 "invoke",方法签名是 methodType(Object.class,Test.class),BSM 方法还附带两个参数分别是实际的方法名:"doSomething" 和一个标志:0

BSM 方法最终调用的是 realBootstrap 方法:

private static CallSite realBootstrap(Lookup caller, String name, int callID, MethodType type, boolean safe, boolean thisCall, boolean spreadCall) {MutableCallSite mc = new MutableCallSite(type); //这里是 MutableCallSite,lambda 表达式用的是 ConstantCallSiteMethodHandle mh = makeFallBack(mc,caller.lookupClass(),name,callID,type,safe,thisCall,spreadCall);mc.setTarget(mh);return mc;
}复制代码

主要的代码是调用 makeFallBack 来获取一个临时的 MethodHandle。因为在第一步 groovy 无法确定接收者(receiver),也是就是 invoke 方法的第一个实参(Test 实例),必须要在第二步确定 CallSite 后才会传递过来。所以方法解析要放在第二步。

protected static MethodHandle makeFallBack(MutableCallSite mc, Class<?> sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall) {MethodHandle mh = MethodHandles.insertArguments(SELECT_METHOD, 0, mc, sender, name, callID, safeNavigation, thisCall, spreadCall, /*dummy receiver:*/ 1); //MethodHandle(Object.class,Object[].class)mh =    mh.asCollector(Object[].class, type.parameterCount()).asType(type);return mh;
}复制代码

这个 fallback 方法其实就是 selectMethodinsertArguments 在这里主要做了一个柯里化的操作,因为selectMethod 的方法签名是

methodType(Object.class, MutableCallSite.class, Class.class, String.class, int.class, Boolean.class, Boolean.class, Boolean.class, Object.class, Object[].class)复制代码

而 inDy 要求的方法签名却是

methodType(Object.class,Test.class)。复制代码

所以得经过 insertArguments 的变换,把确定的值填充进去,用最后的数组参数来接收 inDy 传递的参数。这样这个方法就能够被 inDy 调用了。第一步创建 CallSite 到这里就结束。

第二步,就是 selectMethod 方法的调用,这时候 groovy 已经知道方法的接收者 arguments[0]

public static Object selectMethod(MutableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); selector.setCallSiteTarget();MethodHandle call = selector.handle.asSpreader(Object[].class, arguments.length);call = call.asType(MethodType.methodType(Object.class,Object[].class));return call.invokeExact(arguments);
}复制代码

首先创建一个方法解析器,在这里是 MethodSelector。接着调用 setCallSiteTarget(),这个方法就是用来解析实际的方法。具体的过程还是很复杂的,所以也没法说清楚,大体来说就是确定接收者的 MetaClass,决定这个方法是实际的方法,还是交给 MetaClass 的钩子方法,然后就是创建这个方法的 MethodHandle,然后把这个 MethodHandle 的签名转化为要求的签名。这时 selecor.handle 就是最终调用的方法句柄了。接下来就是最终的方法调用了,到这里 inDy 指令就执行完毕了。

还有一个方法值得留意:

public void doCallSiteTargetSet() {if (!cache) {if (LOG_ENABLED) LOG.info("call site stays uncached");} else {callSite.setTarget(handle);if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");}
}复制代码

这也是为什么用 MutableCallSite 的原因,如果编译器认为这个方法是可以缓存,那么就会把这个 CallSite 绑定到实际的 MethodHandle,后续的调用就不用再重新解析了。

最后

没有相关经验,inDy 还是很不好理解的,学习了 java 8 和 groovy 对 inDy 的应用才有一点大致的认识,文中如果有什么错误,还请帮忙指出。

理解 invokedynamic相关推荐

  1. 小度拆卸_拆卸invokedynamic

    小度拆卸 许多Java开发人员认为JDK的第七版有些令人失望. 从表面上看,仅少数语言和库扩展使它成为了发行版,即Project Coin和NIO2 . 但在幕后,该平台的第七个版本对JVM类型系统进 ...

  2. 拆卸invokedynamic

    许多Java开发人员认为JDK的第七版有些令人失望. 从表面上看,仅少数语言和库扩展使它成为了发行版,即Project Coin和NIO2 . 但在幕后,该平台的第七个版本对JVM类型系统进行了最大的 ...

  3. invokedynamic指令

    Java虚拟机的字节码指令集的数量从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里,一直没有发生任何变化.随着JDK 7的发布,字节码指令集终于迎来了第一位新成员--invok ...

  4. java executor_Java 动态语言支持

    JDK6.0之后提供了脚本引擎功能,让我们可以执行某些脚本语言,特别是javascript(javascript是一门解释性语言,动态性非常好),让JAVA的动态性得到更充分的体现,某些时候可以更加灵 ...

  5. JVM:方法调用之动态类型语言支持。

    Java虚拟机的字节码指令集的数量从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里,一致没有发生任何变化.随着JDK 7的发布,字节码指令集终于迎来了第一位新成员--invok ...

  6. 虚拟机字节码执行引擎

    1 概述 执行引擎是Java虚拟机核心的组成部分之一."虚拟机"是一个相对于"物理机"的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处 ...

  7. c++ 重载 重写_Java | 深入理解方法调用的本质(含重载与重写区别)

    前言 对于习惯使用面向对象开发的工程师们来说,重载 & 重写 这两个概念应该不会陌生了.在中 / 低级别面试中,也常常会考察面试者对它们的理解(隐约记得当年在校招面试时遇到过): 网上大多数资 ...

  8. 深入理解JVM虚拟机(六):虚拟机类加载机制

    1. 概述 虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类记载机制. 与那些在编译时需要进行连接工作 ...

  9. 《深入理解Java虚拟机》笔记5——类加载机制与字节码执行引擎

    第七章 虚拟机类加载机制 7.1 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在J ...

最新文章

  1. R语言包_recharts
  2. 【转】mysqldump的锁表的问题
  3. [FZSZOJ 1029] 观察者加强版
  4. 皮一皮:这是为什么呢???
  5. 每日一皮:天气转凉了,你的长袖穿起来了吗?
  6. mysql charindex_mysql中替代charindex的函数substring_index、find_in_set | 学步园
  7. Bitcoin代码中的Boost signals(1)
  8. 一文说透产品信息结构图的本质
  9. hibernate映射配置文件说明
  10. webrtc2sip项目说明
  11. 金融业大整顿,是要回到十年前的水平吗?
  12. 高等数学18讲(19版)反常积分的计算与敛散性判别
  13. STM32-串口通信详解
  14. 【C补充】指向指针或函数的指针
  15. Flutter开发一个云音乐APP(包含接口地址,亲测可用)
  16. 关于阿里云ECS服务器连接RDS数据库
  17. stm32学习笔记——通用计时器基本原理
  18. [Power BI] 认识Power Query和M语言
  19. 转:目前游戏行业内部主要几款游戏…
  20. 武田宣布多项细胞疗法合作,以推进公司的新型免疫肿瘤学阵容

热门文章

  1. web前端之html从入门到精通
  2. OpenCV3学习(7.4)——图像分割之四(Meanshift算法,PyrMeanShiftFiltering函数)
  3. c语言怎么读取mhx文件内容,C语言重新学习――基础杂类
  4. 推广营销案例_体验式营销助力全屋WIFI推广的成功案例
  5. 2021瑞安高考成绩查询,温州教育网官网登录入口2021瑞安中考成绩查询中招查分系统...
  6. java第一句_Java如何开始第一个项目?
  7. ci框架中引入css,php ci框架中载入css和js文件失败的原因及解决方法
  8. python之字典方法
  9. nginx优化——包括https、keepalive等
  10. MySQL DATE_ADD() 函数