jdk 8 字符串拼接依赖编译期生成的 stringbuilder 指令链,而 jdk 9+ 改用 invokedynamic 指令配合 stringconcatfactory 动态生成最优拼接逻辑,避免中间对象、减少字节码体积并支持运行时优化。

JDK 8 和 JDK 9+ 在字符串拼接的字节码实现上存在本质差异:前者依赖编译期静态生成 StringBuilder 指令链,后者改用运行时动态链接的 invokedynamic 指令,由 StringConcatFactory 负责按需生成最优拼接逻辑。
JDK 8 的字节码:固定 StringBuilder 链式调用
编译器对 a + b + c 这类表达式做静态翻译,每次拼接都生成独立的 new StringBuilder()、多次 append() 和最终 toString() 调用:
- 字节码中明确出现
NEW java/lang/StringBuilder、INVOKESPECIAL StringBuilder.<init></init> - 每个
append()对应一条INVOKEVIRTUAL StringBuilder.append指令 - 循环内使用
+=会导致每轮都新建 StringBuilder 实例,产生大量临时对象 - 即使拼接内容全为字面量(如
"a" + "b" + "c"),也走相同流程;不过编译器会额外优化常量折叠,直接替换成"abc"(仅限编译期可确定场景)
JDK 9+ 的字节码:单条 invokedynamic 指令
编译器不再生成 StringBuilder 相关指令,而是插入一条 invokedynamic 调用点,指向 StringConcatFactory.makeConcatWithConstants 引导方法:
- 字节码中仅见类似
invokedynamic #42:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; - 实际拼接逻辑在首次执行时由 JVM 动态生成并缓存,后续调用直接复用 MethodHandle
- 拼接模板(recipe)如
"\u0001\u0002!"在编译期嵌入,变量位置用 Unicode 占位符标记,运行期只填值,不解析结构 - 默认策略
MH_INLINE_SIZED_EXACT会预计算总长度,直接分配 byte[] 并批量复制,跳过 StringBuilder 扩容和 char→byte 转换开销
关键差异对比
同一行代码 String s = prefix + a + b + suffix; 在不同 JDK 下的字节码表现截然不同:
-
体积:JDK 8 生成 10+ 行字节码;JDK 9+ 通常仅 1–2 行
invokedynamic - 对象创建:JDK 8 必然创建 StringBuilder 实例;JDK 9+ 完全避免中间对象(除非 fallback 到 BC_SB 策略)
- 运行时行为:JDK 8 是确定性调用链;JDK 9+ 是首次链接 + 后续直调,具备热优化潜力
- 调试可见性:JDK 8 的 StringBuilder 步骤可在调试器中逐行观察;JDK 9+ 的拼接逻辑隐藏在生成的匿名类或内联字节码中,不可见
策略可配置性(JDK 9+ 特有)
通过 JVM 参数可切换 StringConcatFactory 的底层策略,影响字节码链接结果:
-
-Djava.lang.invoke.stringConcat=BC_SB:退回到 JDK 8 兼容模式,生成 StringBuilder 字节码 -
-Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT:启用默认高效策略,直接操作字节数组 - 策略选择取决于参数类型、数量、是否含常量等,由 factory 在 bootstrap 阶段决策











