0%

JVM Bytecode之invokedynamic漫谈

概述

invokedynamic是直到Java 7才引入的bytecode指令,当初设计该指令时,是为了方便JVM动态语言的实现,比如Groovy、JRuby、Jython等,而其真正被Java界关注到则是从Java 8开始,因为Java 8引入了Lambda Expression以及Method Reference这两大特性,其实现便是基于invokedynamic指令完成的。

通过invokedynamic可以不再借助Java中的Reflection特性来实现动态特性,比如duck typing。另外,可以在一定程度上规避Reflection性能问题,因为Reflection在每次方法调用时都会做一些检查,而invokedynamic只会在创建method handle时才做这些检查。

invokedynamic基本原理

invokedynamic的整体执行过程如下图所示:
invokedynamic执行过程

当JVM执行invokedynamic指令时,如果是首次执行,则通过invokedynamic指令指定的Bootstrap Method来创建Callsite,并在创建Callsite时会先创建method handle,然后与Callsite绑定,而method handle的创建过程其实也就是一个方法实现的选择过程,这也正是动态的体现,因为这一切都发生在运行时。此外,该Callsite还可以在后续执行中提升执行效率,因为后续执行会直接调用Callsite所绑定的method handle,而不用调用Bootstrap Method,也意味着免去了因方法选择而产生的性能开销。

下面再结合一些代码片段进一步阐述上面提到的invokedynamic执行过程:
invokedynamic执行示意

invokedynamic实战

示例中的IndyString类的concat方法通过invokedynamic指令实现,详见注释。

示例源码

基于Groovy4以及ASM9的invokedynamic演示Groovy Web Console
@Grapes([
@Grab(group = 'org.ow2.asm', module = 'asm', version = '9.4'),
@Grab(group = 'org.ow2.asm', module = 'asm-commons', version = '9.4')
])
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.Handle
import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Type
import org.objectweb.asm.commons.GeneratorAdapter
import org.objectweb.asm.commons.Method
import java.lang.invoke.CallSite
import java.lang.invoke.ConstantCallSite
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import static org.objectweb.asm.Opcodes.ACC_FINAL
import static org.objectweb.asm.Opcodes.ACC_PRIVATE
import static org.objectweb.asm.Opcodes.ACC_PUBLIC
import static org.objectweb.asm.Opcodes.ALOAD
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC
import static org.objectweb.asm.Opcodes.INVOKESPECIAL
import static org.objectweb.asm.Opcodes.PUTFIELD
import static org.objectweb.asm.Opcodes.RETURN
import static org.objectweb.asm.Opcodes.V1_8


// 生成IndyString类,并创建其实例
SimpleClassLoader simpleClassLoader = new SimpleClassLoader(Bootstrap.class.classLoader)
Class<?> indyStringClass = simpleClassLoader.defineClass("IndyString", generateIndyStringClassBytes())
Object indyString = indyStringClass.getConstructor(String.class).newInstance("Hello")

// 调用IndyString的concat方法,并验证执行结果
String result = indyString.getClass().getMethod("concat", String.class).invoke(indyString, "World")
assert 'HelloWorld' == result
println result


/**
* 自定义classloader,用来加载生成的IndyString类
*/
class SimpleClassLoader extends ClassLoader {
SimpleClassLoader(ClassLoader parent) {
super(parent)
}

Class<?> defineClass(final String name, final byte[] b) {
return super.defineClass(name, b, 0, b.length)
}
}

/**
* 通过ASM生成IndyString
*/
static byte[] generateIndyStringClassBytes() throws Exception {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES)
FieldVisitor fv
MethodVisitor mv

cw.visit(V1_8, ACC_PUBLIC | ACC_FINAL, "IndyString", null, "java/lang/Object")
cw.visitSource("IndyString.java", null);

{
fv = cw.visitField(ACC_PRIVATE | ACC_FINAL, "str", "Ljava/lang/String;", null, null)
fv.visitEnd()
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;)V", null, null)
mv.visitCode()
Label l0 = new Label()
mv.visitLabel(l0)
mv.visitVarInsn(ALOAD, 0)
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
Label l1 = new Label()
mv.visitLabel(l1)
mv.visitVarInsn(ALOAD, 0)
mv.visitVarInsn(ALOAD, 1)
mv.visitFieldInsn(PUTFIELD, "IndyString", "str", "Ljava/lang/String;")
Label l2 = new Label()
mv.visitLabel(l2)
mv.visitInsn(RETURN)
Label l3 = new Label()
mv.visitLabel(l3)
mv.visitMaxs(0, 0)
mv.visitEnd()
}
{
GeneratorAdapter ga = new GeneratorAdapter(ACC_PUBLIC, Method.getMethod("java.lang.String concat(java.lang.String)"), null, null, cw)
ga.loadThis()
ga.getField(Type.getType("LIndyString;"), "str", Type.getType(String.class))
ga.loadArg(0)

// (1) 生成调用`String concat(String, String)`方法的invokedynamic指令(预埋“接口”),而该方法的具体实现由此处的`BOOTSTRAP_METHOD`动态指定
ga.invokeDynamic("concat", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", BOOTSTRAP_METHOD)
ga.returnValue()
ga.endMethod()
}
cw.visitEnd()

return cw.toByteArray()
}

// 指定bootstrap方法
@groovy.transform.Field
static final Handle BOOTSTRAP_METHOD =
new Handle(H_INVOKESTATIC, "Bootstrap", "bootstrap",
MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class)
.toMethodDescriptorString(),
false)

/**
* 定义bootstrap类,并实现bootstrap方法
*/
class Bootstrap {
/**
* (2) 确定预埋“接口”的具体实现,由method handle指定。并生成Callsite,将method handle与之绑定
*/
static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type) {
println "caller: ${caller}, name: ${name}, type: ${type}"
MethodHandle methodHandle =
"true" == System.getProperty("enableIndyStringPreview")
? MethodHandles.lookup().findStatic(Functions.class, "groovyConcat", type)
: MethodHandles.lookup().findStatic(Functions.class, name, type)
return new ConstantCallSite(methodHandle)
}
}

/**
* 提供concat方法的多种实现
*/
class Functions {
/**
* (3) 提供预埋“接口”的具体实现,在执行invokedynamic时被调用
*/
static String concat(String s1, String s2) {
return s1 + s2
}

/**
* (3) 提供预埋“接口”的具体实现(preview版本),在执行invokedynamic时被调用
*/
static String groovyConcat(String s1, String s2) {
return "$s1$s2"
}

private Functions() {}
}

示例Bytecode

上面示例中通过ASM生成的`IndyString`字节码
Classfile /D:/_APPS/groovy_apps/indy/IndyString.class
Last modified 2023115日; size 584 bytes
SHA-256 checksum 368db5be43fc93b3fa1746cce06cedd02d8e856224c1eb48b26abe1d62067746
Compiled from "IndyString.java"
public final class IndyString
minor version: 0
major version: 52
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
this_class: #2 // IndyString
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 2
Constant pool:
#1 = Utf8 IndyString
#2 = Class #1 // IndyString
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 IndyString.java
#6 = Utf8 str
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 (Ljava/lang/String;)V
#10 = Utf8 ()V
#11 = NameAndType #8:#10 // "<init>":()V
#12 = Methodref #4.#11 // java/lang/Object."<init>":()V
#13 = NameAndType #6:#7 // str:Ljava/lang/String;
#14 = Fieldref #2.#13 // IndyString.str:Ljava/lang/String;
#15 = Utf8 concat
#16 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#17 = Utf8 Bootstrap
#18 = Class #17 // Bootstrap
#19 = Utf8 bootstrap
#20 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#21 = NameAndType #19:#20 // bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#22 = Methodref #18.#21 // Bootstrap.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#23 = MethodHandle 6:#22 // REF_invokeStatic Bootstrap.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#24 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#25 = NameAndType #15:#24 // concat:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#26 = InvokeDynamic #0:#25 // #0:concat:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#27 = Utf8 Code
#28 = Utf8 SourceFile
#29 = Utf8 BootstrapMethods
{
public IndyString(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #14 // Field str:Ljava/lang/String;
9: return

public java.lang.String concat(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #14 // Field str:Ljava/lang/String;
4: aload_1
5: invokedynamic #26, 0 // InvokeDynamic #0:concat:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
10: areturn
}
SourceFile: "IndyString.java"
BootstrapMethods:
0: #23 REF_invokeStatic Bootstrap.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:

invokedynamic应用场景

从本质上来看,invokedynamic关注的只有方法签名(方法名、输入以及输出),可以理解为更抽象的一种“接口”,这比传统的interface更加灵活,因为invokedynamic不要求被调用方法在同一棵类型继承树中。而至于具体实现都是在运行时才确定的,所以正如概述所提到的,invokedynamic指令适合用来实现一些动态特性,比如动态语言Groovy从2.0起便已开始使用invokedynamic实现方法调用。另外,如果方法的具体实现还有待完善,且后续的完善可能不限定于某个interface的具体实现,那么也可以借助invokedynamic在bytecode层面确定好“接口”,至于具体实现可以日后不断调整。其实invokedynamic在Java中已经有不少应用,包含但不限于以下场景:

  • Lambda Expression以及Method Reference的实现
  • String拼接,即字符串的+操作
    Java 9之前,编译时固化了+操作实现,即通过StringBuilder实现拼接,而从Java 9开始,使用invokedynamic指令,延后具体的拼接操作至运行时,为后续Java版本对字符串拼接的不断优化提供可能
  • Record的toStringhashCode以及equals方法
    Record的字段不论数量还是类型都随Record的定义不同而各有差异,通过invokedynamic可以简化toStringhashCode以及equals方法,即不用在编译时生成所有的方法实现,这样class文件的大小也不会因字段不同而改变
  • Reflection的替代
    既然invokedynamic具有众多优点,何不考虑通过invokedynamic重构既有的Reflection实现呢?Java 18也有此想法,并在JEP 416提出并实现。而正所谓英雄所见略同,在我的个人项目fast-reflection中,也尝试过实现该想法。

参考资料

  • The Java Virtual Machine Specification - Java SE 17 Edition
  • Groovy InvokeDynamic support