1. 概述

JVM 和 Java 编译器一直在默默优化各种细节,哪怕是最不起眼的操作也不会放过。字符串拼接就是其中一个被深度优化的场景。

本文将深入剖析 Java 9 引入的一项重要优化:基于 invokedynamic 的字符串拼接机制。这不仅是性能的提升,更体现了 JVM 在运行时动态优化上的强大能力。

2. Java 9 之前的实现方式

在 Java 9 之前,非字面量的字符串拼接(比如变量拼接)底层是通过 StringBuilder 实现的。来看一个简单的例子:

String concat(String s, int i) {
    return s + i;
}

使用 javap -c 查看其字节码:

java.lang.String concat(java.lang.String, int);
  Code:
     0: new           #2      // class StringBuilder
     3: dup
     4: invokespecial #3      // Method StringBuilder."<init>":()V
     7: aload_0
     8: invokevirtual #4      // Method StringBuilder.append:(LString;)LStringBuilder;
    11: iload_1
    12: invokevirtual #5      // Method StringBuilder.append:(I)LStringBuilder;
    15: invokevirtual #6      // Method StringBuilder.toString:()LString;

关键点:

  • 编译器自动将 + 操作符翻译为 StringBuilder 的创建、追加和 toString() 调用。
  • 这种方式本身已经相当高效,是经过充分验证的工程实践。

但这种方式的局限性在于:优化策略固化在字节码中,一旦类文件生成,底层拼接逻辑就无法再改变。Java 9 的 invokedynamic 方案正是为了解决这个痛点。

3. invokedynamic 机制

从 Java 9 开始(作为 JEP 280 的一部分),字符串拼接改用 invokedynamic 指令实现。

3.1 核心思想

⚠️ 核心动机:实现“运行时可变”的拼接策略。
这意味着,JVM 可以在不修改字节码的前提下,动态替换底层的拼接实现。即使你的应用是用旧版本 JDK 编译的,只要运行在新版本 JVM 上,也能自动享受到最新的优化策略,无需重新编译

其他优势:

  • 字节码更简洁、更小。
  • 减少了对特定 API(如 StringBuilder)的硬编码依赖,更具灵活性。

3.2 整体流程

我们可以把一次字符串拼接看作一个函数调用:输入参数(字符串、整数等),输出结果(拼接后的字符串)。

新的 invokedynamic 机制工作流程如下:

  1. 定义函数签名:例如 (String, int) -> String
  2. 准备实际参数:比如拼接 "The answer is "42
  3. 调用 Bootstrap Method:JVM 在首次执行 invokedynamic 指令时,会调用一个预定义的“引导方法”(Bootstrap Method),传入函数签名、参数和一些元信息。
  4. 生成并链接实现:引导方法根据签名和参数,动态生成一个最优的拼接逻辑,并封装在一个 MethodHandle 中。
  5. 执行拼接:后续调用直接通过 MethodHandle 执行生成的逻辑,得到最终字符串。

Indy Concat

📌 一句话总结:编译时只生成“规范”,运行时才“链接”具体实现。 这种解耦是 invokedynamic 的精髓所在。

4. 字节码与链接机制

让我们看看 Java 9+ 编译器为同一个 concat 方法生成的字节码:

java.lang.String concat(java.lang.String, int);
  Code:
     0: aload_0
     1: iload_1
     2: invokedynamic #7,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;I)LString;
     7: areturn

对比 Java 8:

  • ❌ 旧版:7 条指令,明确创建 StringBuilder 并调用其方法。
  • ✅ 新版:仅 3 条指令,核心逻辑被一个 invokedynamic 指令替代,简洁粗暴

这里的 (LString;I)LString 是方法描述符,表示接受一个 String 和一个 int,返回一个 String

4.1 Bootstrap Method 表

使用 javap -c -v 查看更详细的信息,会发现一个 BootstrapMethods 表:

BootstrapMethods:
  0: #25 REF_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;
    Method arguments:
      #31 \u0001\u0001
  • **makeConcatWithConstants**:这是 StringConcatFactory 类中的一个静态方法,作为引导方法。
  • 当 JVM 首次遇到这个 invokedynamic 指令时,就会调用此引导方法。
  • 引导方法会返回一个 ConstantCallSite,它内部持有一个指向实际拼接逻辑的 MethodHandle
  • 后续所有对该 invokedynamic 的调用,都会直接跳转到这个 MethodHandle 指向的代码,效率极高。

Indy

4.2 关键参数

引导方法接收几个关键参数:

  • MethodType:即 (LString;I)LString,描述了拼接操作的签名。
  • \\u0001\\u0001:这是一个“配方”(recipe),定义了字符串的结构。

5. 配方(Recipe)机制

配方是 invokedynamic 拼接的核心优化之一,它区分了字符串中的常量部分变量部分

5.1 示例分析

考虑一个 Person 类:

public class Person {
    private String firstName;
    private String lastName;

    // constructor

    @Override
    public String toString() {
        return "Person{" +
          "firstName='" + firstName + '\'' +
          ", lastName='" + lastName + '\'' +
          '}';
    }
}

其字节码为:

 0: aload_0
 1: getfield      #7        // Field firstName:LString;
 4: aload_0
 5: getfield      #13       // Field lastName:LString;
 8: invokedynamic #16,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;LString;)L/String;
 13:areturn

对应的 BootstrapMethods 表:

BootstrapMethods:
  0: #28 REF_invokeStatic StringConcatFactory.makeConcatWithConstants // truncated
    Method arguments:
      #34 Person{firstName=\'\u0001\', lastName=\'\u0001\'} // The recipe

5.2 配方的作用

  • **\\u0001**:这是一个占位符(placeholder),代表一个动态参数。
  • **Person{firstName=', ', lastName=', '}**:这些都是字符串常量(literal)。

优势:

  • 大幅减少参数数量:无论字符串模板多复杂,引导方法只需要接收所有动态参数(firstName, lastName)和一个配方字符串。
  • 提升效率:避免了将大量常量字符串作为参数传递,减少了栈操作和方法调用开销。

可以把配方理解为一个字符串模板,其中 \\u0001 就是待填充的变量。

6. 字节码的两种“风味”(Flavors)

JVM 提供了两种生成 invokedynamic 字节码的策略,可通过编译器选项控制。

6.1 indy-with-constants(默认)

  • 特点:使用 makeConcatWithConstants 引导方法和配方机制。
  • 优点:字节码紧凑,参数少。
  • 触发方式:Java 9+ 默认行为。

6.2 indy-only-args

  • 特点:不使用配方,将字符串中的每一个部分(无论是常量还是变量)都作为独立参数传递。
  • 触发方式:编译时加上 -XDstringConcat=indy 选项。
public java.lang.String toString();
    Code:
       0: ldc           #16      // String Person{firstName=\'
       2: aload_0
       3: getfield      #7       // Field firstName:LString;
       6: bipush        39
       8: ldc           #18      // String , lastName=\'
      10: aload_0
      11: getfield      #13      // Field lastName:LString;
      14: bipush        39
      16: bipush        125
      18: invokedynamic #20,  0  // InvokeDynamic #0:makeConcat:(LString;LString;CLString;LString;CC)LString;
      23: areturn
  • 引导方法makeConcat
  • 参数:7 个,分别对应 "Person{firstName='", firstName, \', ", lastName='", lastName, \', }

⚠️ 注意:这种模式生成的字节码更长,参数更多,通常不推荐,主要用于测试或特定场景。

6.3 回到过去

你甚至可以用 -XDstringConcat=inline 选项,让编译器回到 Java 8 的 StringBuilder 方式,这在性能对比或排查问题时非常有用。

7. 拼接策略(Strategies)

引导方法最终会返回一个 MethodHandle,这个 MethodHandle 指向的才是真正的拼接逻辑。JVM 内部有多种策略来生成这个逻辑。

7.1 六种策略

策略名称 描述 特点
BC_SB 字节码 - StringBuilder 在运行时动态生成与 Java 8 完全相同的 StringBuilder 字节码,并用 Unsafe.defineAnonymousClass 加载。
BC_SB_SIZED 字节码 - StringBuilder (有初始容量) BC_SB 类似,但会尝试预估 StringBuilder 的初始容量,减少扩容。
BC_SB_SIZED_EXACT 字节码 - StringBuilder (精确容量) 在生成字节码前,先将所有参数转换为字符串,精确计算出所需容量。
MH_SB_SIZED MethodHandle - StringBuilder (有初始容量) 使用 MethodHandle 直接调用 StringBuilder API,通过 MethodHandle 链接,会预估容量。
MH_SB_SIZED_EXACT MethodHandle - StringBuilder (精确容量) MH_SB_SIZED 类似,但能精确计算容量。
MH_INLINE_SIZE_EXACT MethodHandle - 内联 (精确容量) 默认策略。不使用 StringBuilder,而是直接操作 byte[] 数组,自己管理内存和拼接逻辑,就像 StringBuilder 内部做的那样。

7.2 如何切换策略

可以通过 JVM 系统属性在运行时切换策略:

-Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT

MH_INLINE_SIZED_EXACT 替换为上述任意策略名称即可。

8. 结论

Java 9 引入的 invokedynamic 字符串拼接机制,是一次非常成功的底层优化:

  • 解耦编译时与运行时:让 JVM 能在不修改字节码的情况下升级优化策略。
  • 性能提升:默认的 MH_INLINE_SIZE_EXACT 策略通过内联和精确容量计算,达到了极高的效率。
  • 灵活性高:提供了多种字节码生成方式和运行时策略,方便调试和调优。

这项优化是 JVM 持续演进的缩影。对于开发者而言,虽然日常编码无需关心这些细节,但理解其背后原理,有助于我们写出更高效的代码,也能在遇到性能问题时,知道从哪个方向去排查。

延伸阅读


原始标题:String Concatenation with Invoke Dynamic