欢迎光临散文网 会员登陆 & 注册

invokedynamic指令的个人理解

2023-08-16 03:04 作者:lovexyn0827  | 我要投稿

前言

试着在3000字以内讲清自己对Java 7中引入的invokedynamic的理解。这篇文章主要省去了一些对Java API与JVM规范的引述,并侧重于如何去理解以及该指令本身的使用的基本思路,而不是讲解具体的使用规范。

文章如有缺漏错误之处,欢迎反馈。

此处假定读者掌握以下知识:

  • Java语言

  • JVM的基本运行模型

  • 基本的字节码指令

  • ASM的基本结构与简单使用

概述

invokedynamic这一指令由两个词语组成,即invoke(v., 调用)与dynamic(adv. & adj., 动态的),顾名思义,这条指令可以访问一个在运行时动态地确定而非硬编码于类文件中的目标。

简单来说,这条指令的执行包括以下几个步骤:

  1. 如果是第一次执行,调用指令附加参数中给出的一个方法(BootstrapMethod)来确定该指令的行为,如调用哪一个方法或者是访问哪一个字段。

  2. 按照第一次执行时确定的行为执行具体操作。

也就是说,invokedynamic指令的行为是在第一次被使用时才被确定的。

方法句柄MethodHandle

方法句柄代表了一个对类成员的基本操作,包括读写字段和调用方法等。可以认为,一个方法句柄包含了一个具体成员的位置与其执行的具体操作两个属性。

可以通过由java.invoke.MethodHandles.lookup()方法获取的MethodHandles.Lookup实例中提供的工厂方法获取MethodHandle实例。那些工厂方法大致可以分为两类,一类是名称类似findXXX()的方法,支持使用类似于反射的方式获取方法句柄;另一类工厂方法的名称类似于unflectXXX(),支持为已有的FieldMethod对象指定具体操作以将其转换为方法句柄。

第一类工厂方法的形式大多类似于findXXX(class, name, type),三个参数分别为定义目标成员的类相应的Class实例、目标成员的名称与目标成员的确切类型(或方法签名)。唯一的例外是findConstructor方法,因为它不需要显式地指明名称。在获取操作方法的方法句柄时,需要使用一个MethodType实例来表示方法签名,这个实例可以通过工厂方法MethodType.methodType()获取。

第二类工厂方法的形式比较简单,只接受一个FieldMethod实例,此处不再细说。

另外,这些工厂方法在执行时通常会检查曾获取所用的Lookup实例的类是否能访问目标成员,如果失败则抛出一个IllegalAccessException。在Java 9及以后的版本中,我们可以使用privateLookupIn()工厂方法获取可以访问调用类以外的其他类的私有成员的Lookup实例。对于第二类工厂方法,我们可以预先在用到的反射对象上调用setAccessible(true)来禁用这一访问检查,或许这也是Java 8中唯一可以获取任意方法句柄的方案。

MethodHandles类中也提供了多个工厂方法以获取或变换一些方法句柄,此处不再赘述。

调用站点CallSite

调用站点是一个方法句柄的容器,方法句柄只有被包含在调用站点中时才可以在invokedynamic指令中使用。

调用站点可以是可变的(MutableCallSiteVolatileCallSite),也可以是不可变的(ConstantCallSite)。不可变的调用站点可能会更高效,因为JVM可以对其进行一些优化。

也可以创建自己的CallSite子类以实现一些自定义逻辑,如在调用次数超过一定值的前后提供不同的方法句柄。

BootstrapMethod

BootstrapMethod,简称“BSM”,是一个用于在运行时确定invokedynamic指令的具体行为的方法。当然,Java 11中引入的动态常量也使用了相同的技术,但是这超出了本文的范围,此处不再详述。

一个BootstrapMethod通常是一个静态方法,前三个参数的类型必须依次为:

  1. MethodHandles.Lookup

  2. String

  3. MethodType

这些参数后面还可以附加几个参数用于传递一些附加信息。Java 10及之前的版本中,附加的参数类型可以为intfloatlongdoubleStringMethodTypeMethodHandle。同时,该方法必须返回一个CallSite实例。下方是一个简单的BootstrapMethod的定义:

Java 11中也允许借助动态常量技术使用其他类型的附加参数,以后会对其进行专门讲解。 在执行该方法时,传入的参数依次是:

  • invokedynamic所在类通过MethodHandles.lookup()工厂方法获取的MethodHandle实例;

  • invokedynamic指令指定的名称;

  • 描述该invokedynamic行为的方法描述符

  • 附加的零至多个参数 JVM标准中规定,也可以使用构造器作为BootstrapMethod,只要那个构造器能够构造出一个CallSite类型的对象。具体实现与使用静态方法类似,此处不再赘述。 有必要说明,通过附加参数给出的MethodHandle不可以访问invokedynamic指令所在类不可访问的成员,否则在类的解析阶段会因为访问检查出错而抛出IllegalAccessError

invokedynamic指令的格式

JVM字节码中invokedynamic指令的格式非常简单:

其中,两个index字节共同组成了一个指向常量池中一个CONSTANT_InvokeDynamic_info结构的索引,该结构直接或间接地提供了以下信息:

  • invokedynamic的名称与对应的方法描述符;

  • BootstrapMethod方法信息,可以指定一个静态方法或构造器;

  • BootstrapMethod方法的附加参数。 某种意义上也就是说,这三项信息是invokedynamic方法的固定参数。

在ASM中使用invokedynamic指令

Core API

可以由visitInvokeDynamicInsn()方法获取或创建invokedynamic指令,其定义如下:


  • name:该invokedynmaic指令的名称,只是传入BootstrapMethod一个常量,可以按需要(随便)设定;

  • descriptor:描述invokedynmaic指令行为方法一个方法描述符,应与BootstrapMethod返回的CallSite实际相应的方法签名相符;

  • bootstrapMethodHandle:一个Handle实例,提供的信息与MethodHandle相似,指定了BootstrapMethod具体实现的位置;

  • bootstrapMethodArguments:传入BootstrapMethod的附加参数,可以为IntegerFloatLongDoubleStringorg.objectweb.asm.Typeorg.objectweb.asm.Handle几种类型。真正传入BootstrapMethod时基本类型的封装类会被拆箱为基本类型,而ASM提供的TypeHandle类分别会被转换为包含同样信息的MethodTypeMethodHandle实例。Java 11和ASM 7.0之后也可以传入org.objectweb.asm.ConstantDynamic来指定一个在运行时动态获取的常量,它的值在调用BootstrapMethod时会被计算出并作为参数传入其中。

    其中Handle类是ASM提供的用于记录MethodHandle实例属性的一个类,可以通过以下构造器获取:


  • tag:用于描述该Handle类型的一个数学,决定了其对应的MethodHandle的行为,可以将ASM库中Opcodes接口中名为的H_XXX字段(如Opcodes.H_GETFIELD)传入,在对JVM有所了解的前提下从名称分析其含义还是比较简单的。

  • owner:包含目标成员的类的内部名称。

  • name:目标成员的名称。

  • descriptor:描述该invokedynamic指令行为的方法描述符。

  • isInterface:包含目标成员的类是否是接口。

Tree API

Tree API中的InvokeDynamicInsnNode对应一个invokedynamic指令,使用方法与Core API相似,此处不再赘述。

invokedynamic指令的应用

在Java语言中invokedynamic指令两个最常见个用途是实现Lambda表达式与方法引用,具体的实现方式超出了本文的范围,本文中不再详述。

此处我们真正要探讨的是invokedynamic自身的应用。 举个例子,假设一个应用程序需要从一个配置文件中获取真正的Main类,那么这个应用的入口类可以用反射这样实现:

如果不使用反射呢?我们也可以生成一个使用invokedynamic的入口类! 可以使用以下代码生成main()方法的字节码:

这时,可以这样实现BootstrapMethod

或许这个例子有些牵强,但这确实在一个简单的情景下为我们展示了invokedynamic指令的基本用法。

另一个比较接近实际的例子是自己在上个月做的AccessingPath编译器中实现的使用字节码访问私有成员的功能。因为直接使用反射的性能较低,那里使用了invokedynamic来访问私有字段与方法。具体实现可以在 https://github.com/lovexyn0827/MessMod/tree/master/src/main/java/lovexyn0827/mess/util/access CompiledPathBytecodeHelper.addInvoker()下找到。



invokedynamic指令的个人理解的评论 (共 条)

分享到微博请遵守国家法律