Java 基础之 stringConcatViaInvokeDynamic
Java 9 之后的字符串拼接原理
这里是 j19 的字节码
不想写前情提要了 看图:
j9 之后字符串拼接会编译为一个 invokedynamic 调用
tldr:
这里的原理就是运行时根据拼接的参数 产生了一组新的方法来执行字符串链接
前置资料
- JEP 280: Indify String Concatenation (openjdk.org)
- https://openjdk.org/jeps/210
- https://openjdk.org/jeps/303
翻译策略
tldr: 核心思想就是精确计算拼接后的长度,直接生成对应长度的未初始化的 byte[]直接拷贝填充后直接传递给 String 的一个私有构造器(如下)
String(byte[] value, byte coder) {
this.value = value;
this.coder = coder;
}
具体代码不贴了 解释起来比较复杂 直接贴个网址吧 https://cr.openjdk.java.net/~shade/8085796/notes.txt
- BC_SB 或称之为 bytecode StringBuilder 的策略在运行时生成相同的StringBuilder字节码。然后它通过Unsafe.defineAnonymousClass方法加载生成的字节码,这种方式只是将翻译旧有的翻译策略从编译期移动到了运行时
- BC_SB_SIZED 或称之为 bytecode StringBuilder、but sized 的策略将尝试猜测StringBuilder 所需的容量。除此之外,它与以前的方法相同。猜测容量可能有助于 StringBuilder 在不调整底层字节数组长度的情况下执行拼接操作
- BC_SB_SIZED_EXACT 或称之为 bytecode StringBuilder, but sized exactly 是基于StringBuilder的策略,可以精确计算所需的存储。要计算确切的大小,首先,它将所有参数转换为字符串
- MH_SB_SIZED 或称之为 MethodHandles StringBuilder, sized 是基于 MethodHandles 的策略,并最终调用StringBuilderAPI 进行串联。此策略还可以对所需容量进行有根据的猜测
- MH_SB_SIZED_EXACT 与前一个类似,只是它完全准确地计算了必要的容量
- MH_INLINE_SIZE_EXACT 预先计算所需的存储,并直接维护其 byte[] 来存储串联结果。此策略会直接生成内联的字节码,通过调用各种私有的 api,相当于将 StringBuilder 的拼接逻辑内联到字符串拼接处
来看个实例?
public static String call(Object foo, int bar) {
return "arg0: " + foo + ",arg1:" + bar;
}
字节码
public static call(Ljava/lang/Object;I)Ljava/lang/String;
// parameter foo
// parameter bar
L0
LINENUMBER 20 L0
ALOAD 0
INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
ILOAD 1
INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;I)Ljava/lang/String; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
// arguments:
"arg0: \u0001, arg1: \u0001"
]
ARETURN
L1
LOCALVARIABLE foo Ljava/lang/Object; L0 L1 0
LOCALVARIABLE bar I L0 L1 1
MAXSTACK = 2
MAXLOCALS = 2
看起来调用了 java/lang/invoke/StringConcatFactory.makeConcatWithConstants 这个方法
那我们再来看看这个方法签名吧
public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
String name,
MethodType concatType,
String recipe,
Object... constants)
- concatType 包含要创建的匿名方法的方法签名信息其中的参数信息包含要联接的变量的类型信息 即需要涉及到哪几个变量的捕获,以需要捕获的变量作为入参,string 作为返回值描述需要生成的函数,在这个例子里面最后生成的是[String,int]String (注意看上面的字节码 Object 这里被 desugar 为 String.value(foo) 已经变成 String 类型了)
- recipe 则是一个类似于模板的字符串 这里是"arg0: \u0001, arg1: \u0001" (\u0001 类似于占位符,用来表示这里该插入一个变量)
- name 就是 makeConcatWithConstants
- Lookup 则是 caller 的名字
剩下的部分就是生成字节码了 这里复用了 lambda 表达式的生成逻辑,有兴趣看看 LambdaForm::compileToBytecode 的代码,或者带这个参数 java.lang.invoke.MethodHandle.TRACE_RESOLVE=true 看一下控制台输出,也可以通过 java.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true 看一下生成的代码
注意这里生成的方法实际上会归于一个隐藏类(hidden class) 下一步就是加载这个字节码了,通过 InvokerBytecodeGenerator::loadMethod 方法实现可得 就是走的 ClassLoad 的 native 方法——ClassLoader.defineClass0(loader, lookup, name, b, 0, b.length, pd, initialize, flags, classData);
现在 我们直接看生成之后的代码吧
String anonymous(Object foo, int bar) {
String os = StringConcatHelper.stringOf(foo);
//计算确切 byte 长度[]
long t1 = StringConcatHelper.mix(0, "arg0:"); //t1=5
long t2 = StringConcatHelper.mix(t1, ",arg1:");//t2=11
//这个是 StringConcatHelper::mix(long lengthCoder, int value)
long t3 = StringConcatHelper.mix(t2, bar) //t3 = 12
long t4 = StringConcatHelper.mix(t4, os) //t4 = 37
//获取一个未初始化的 byte[]
byte[] buf = StringConcatHelper.newArray(t4);
//StringConcatHelper::prepend(long indexCoder, byte[] buf, int value, String prefix) 将 int 转为 char 然后插入,arg1:
//为了防止多余拷贝这走了一个私有 api 直接获取",arg1:"内部的 byte 数组拷贝到 buf 里面
long t5 = StringConcatHelper.prepend(t4, buf, bar, ",arg1:"); //t5=30
//
long t6 = StringConcatHelper.prepend(t5, buf, os, "arg0:"); //t6=0
return StringConcatHelper.newString(buf, t6);
}
看起来还有点遗漏?
StringConcatHelper.stringOf(foo)
如果为 foo 空怎么办? 看代码:
static String stringOf(Object value) {
String s;
return (value == null || (s = value.toString()) == null) ? "null" : s;
}
特别简单的场景也要走这个流程是不是有点重?
其实你在代码里面能看到几个 fast path,这里讲几个
将字符串与一个变量连接,或将两个变量连接
public static String call(Object bar){
return "arg0:" + bar;
}
会走到 StringConcatHelper::simpleConcat 方法里面
其等价于
String anonymous(Object foo, Object bar){
String s1 = StringConcatHelper.stringof(foo); // 这里的 foo 是 arg0:
String s2 = StringConcatHelper.stringof(bar); // bar java.lang.Object@1d81eb93
long t1 = StringConcatHelper.mix(0,s1); // t1 = 5
long t2 = StringConcatHelper.mix(t1,s2); //t2=30
byte[] buf = StringConcatHelper.newArray(t2);
long t3 = StringConcatHelper.prepend(t2,buf,s2); //t3 = 5
long t4 = StringConcatHelper.prepend(t3,buf,s1); //t4 =0
return StringConcatHelper.newString(buf, t4);
}
如果字符串里面本来就有\u0001 怎么办
"Hello\u0001 " + foo + "!";
还记得有个 makeConcatWithConstants 入参 Object... constants 吗?
这个时候这里面就有值了 constants
: [ "Hello\u0001 " ]
我们最多可以连接几个变量?
200 个
if (concatType.parameterSlotCount() > MAX_INDY_CONCAT_ARG_SLOTS) {
throw new StringConcatException("Too many concat argument slots: " +
concatType.parameterSlotCount() +
", can only accept " +
MAX_INDY_CONCAT_ARG_SLOTS);
}
如何回退 javac 生成策略?
在编译时,可以使用选项编译为使用旧字符串生成器的实现。 -XDstringConcat=inline
顺便说一下,此选项可以具有以下值:
值 | 操作 |
---|---|
inline | StringBuilder 实现 |
indy | 使用 StringConcatHelper#makeConcat |
indyWithConstants(默认值) | 使用 StringConcatHelper#makeConcatWithConstants |
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Java 基础之 socket
下一篇: Jvm 常量池
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论