简短版本:从 Java 9 开始,Java 使用 invokedynamic 来连接字符串。
让我们稍微分解一下:
Invokedynamic有两个步骤:
- 当第一次调用该指令时,将调用引导方法。当它返回时,调用站点将链接到引导方法的结果。
- 后续调用将直接调用目标方法句柄 https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/invoke/MethodHandle.html.
The CallSite https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/invoke/CallSite.html只是一个持有者方法句柄 https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/invoke/MethodHandle.html。取决于CallSite
使用该站点的子类稍后可能会重新链接。
如果我们查看指令,我们会在末尾看到以下内容:
#0:makeConcatWithConstants:(I)Ljava/lang/String;
第一部分 (#0
) 表示:引导方法#0。
第二部分是名称 - 它被传递给引导方法,并且可能会也可能不会在那里使用。
第三部分是结果目标的方法类型。在我们的例子中:一个方法需要int
并返回一个java.lang.String
.
如果我们现在看一下引导方法#0,我们会看到一个方法引用,这里是StringConcatFactory.makeConcatWithConstants(...) https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/invoke/StringConcatFactory.html#makeConcatWithConstants(java.lang.invoke.MethodHandles.Lookup,java.lang.String,java.lang.invoke.MethodType,java.lang.String,java.lang.Object...)。
我们还看到还有一个附加参数:字符串"text + String: \u0001"
.
现在,引导方法的工作是返回一个 MethodHandle(在 CallSite 内),在本例中它执行此字符串连接。但它如何进行字符串连接(StringBuilder、String.format、字节码旋转、链接 MethodHandles...)对于实际的类来说并不重要。它只想连接字符串。
让我们尝试手动模拟该行为。毕竟,bootstrap 方法是一个普通的 Java 方法:
public static void main(String[] args) throws Throwable {
CallSite cs = StringConcatFactory.makeConcatWithConstants(MethodHandles.lookup(),
"makeConcatWithConstants", MethodType.methodType(String.class, int.class),
"text + String: \u0001");
int x = 2;
String result = (String) cs.dynamicInvoker().invokeExact(x);
System.out.println(result);
x = 3;
result = (String) cs.dynamicInvoker().invokeExact(x);
System.out.println(result);
}
(虚拟机做了更多的事情,比如它记住结果并且不会再次调用引导方法,但对于我们的小例子来说这已经足够好了)。
此时,我们可以深入了解引导方法如何完成其工作。
事实证明:您可以将虚拟机配置为使用不同的策略。
而且它利用了它在内部的特权地位java.base
访问不复制数组的 java.lang.String 的包私有构造函数 - 如果之后不修改内容,这是安全的。
默认策略是 MethodHandle 链接。
好消息是:如果有人在某个时候编写了更好的策略,您的程序将从中受益 - 无需重新编译。