StringConcatFactory / 字符串拼接优化源码
概述
字符串拼接是 Java 程序中最频繁的操作之一。JDK 8 及之前,"a" + "b" 会被编译成 new StringBuilder().append("a").append("b").toString() 序列;JDK 9(JEP 280) 起,javac 改为生成 invokedynamic 调用点,由运行时引导方法 StringConcatFactory 决定如何拼接。
这一改动的价值在于:策略从"编译期写死"变成"运行时选择"——默认策略 MH_INLINE_SIZED_EXACT 在拼接前精确计算最终字符串长度,一次性分配 byte[] 反向填充,彻底避免 StringBuilder 的扩容与二次拷贝。本文基于 OpenJDK 21 源码拆解这条链路。
核心源码解析
① "a" + "b" 的编译差异
| 版本 | 字节码形态 | 说明 |
|---|---|---|
| JDK 8 | new StringBuilder + append + toString | 编译期写死,扩容可能触发数组拷贝 |
| JDK 9+ | invokedynamic(StringConcatFactory) | 运行时选择策略,可随 JVM 版本演进 |
JDK 9+ 的字节码示意:
invokedynamic #makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;- 编译期无法确定拼接目标(非常量)时,javac 生成
invokedynamic调用点,MethodType描述拼接参数类型(如(String, int)→String)。 makeConcatWithConstants还带一个 recipe 字符串(如"\u0001 is \u0001"):\u0001表示参数槽位,其余为常量文本直接嵌入——常量部分不必再拼接,省去运行时开销。- 全部为常量时(
"a" + "b"),javac 在编译期直接折叠为"ab",根本不生成拼接字节码(见 ⑦)。
② StringConcatFactory.makeConcat(...) 的引导方法
java
public final class StringConcatFactory {
// 无 recipe 版本:参数直接按顺序拼接
public static CallSite makeConcat(MethodHandles.Lookup lookup, String name, MethodType concatType) {
return doStringConcat(lookup, name, concatType, false, null);
}
// 带 recipe 版本:常量与参数混合拼接
public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup, String name,
MethodType concatType, String recipe,
Object... constants) {
return doStringConcat(lookup, name, concatType, true, recipe);
}
}- 引导成功后返回
ConstantCallSite,调用点被 JIT 固定并内联——后续拼接调用直接走生成的MethodHandle,不再经过引导。 makeConcatWithConstants的constants是 recipe 中\u0002占位符对应的常量参数,同样免于运行时计算。doStringConcat内部先解析 recipe(StringConcatFactory的 recipe 解析器把\u0001/\u0002/\u0003分别映射为参数/常量/动态常量),再按当前默认策略生成句柄链。
③ Strategy 的 6 种策略
java
enum Strategy {
BC_SB, // 字节码模板:StringBuilder.append 序列
BC_SB_SIZED, // 字节码模板:先计算大小的 StringBuilder
BC_SB_SIZED_EXACT, // 字节码模板:精确大小分配
MH_SB_SIZED, // MethodHandle 组合:StringBuilder 预分配
MH_SB_SIZED_EXACT, // MethodHandle 组合:StringBuilder 精确分配
MH_INLINE_SIZED_EXACT // MethodHandle 组合:直接 byte[] 内联(默认)
}| 策略 | 分配方式 | 是否扩容 | 默认启用 |
|---|---|---|---|
BC_SB | 默认大小 StringBuilder | 可能扩容 | 否 |
BC_SB_SIZED | 估算大小 | 可能扩容 | 否 |
BC_SB_SIZED_EXACT | 精确大小 | 否 | 否 |
MH_SB_SIZED | MethodHandle + 估算 | 可能扩容 | 否 |
MH_SB_SIZED_EXACT | MethodHandle + 精确 | 否 | 否 |
MH_INLINE_SIZED_EXACT | 直接 byte[] 精确 | 否 | 是 |
- 前三种生成字节码模板(
Generator类拼接字节码指令),后三种生成MethodHandle组合(getMethodHandle组装)。 - 通过 JVM 参数
-Djava.lang.invoke.stringConcat=BC_SB等可切换策略,用于对比测试。 - 默认策略
MH_INLINE_SIZED_EXACT在 JDK 15 起成为默认:不经过StringBuilder,直接管理byte[]。
④ MH_INLINE_SIZED_EXACT 策略
引导时生成的句柄链(getMethodHandle 简化):
java
// 1) newArray:根据总长度 + coder 分配精确的 byte[]
MethodHandle newArray = MethodHandles.insertArguments(
lookupStatic(Lookup, "newArray", byte[].class, long.class), 0, 0);
// 2) 对每个参数依次 prepend(反向填充)
MethodHandle prepend = ... // 每个拼接片段一个句柄
// 3) newString:用 coder 与长度把 byte[] 转成 String
MethodHandle newString = lookupStatic(lookup, "newString", String.class, byte[].class, long.class);- 两参数快速路径
simpleConcat(Object first, Object second)(StringConcatHelper中)被单独优化:newArray→prepend(second)→prepend(first)→newString,共 4 个操作完成一次拼接。 - 多参数路径:用
mixLen/mixCoder先算出总长度与编码,再按从后到前顺序逐段prepend。 - 关键点:只分配一次数组、只做一次字符串构造,没有中间
StringBuilder对象,也没有任何扩容拷贝。
⑤ StringConcatHelper.prepend(long indexCoder, byte[] buf, Object value) 反向填充
java
// jdk.internal.misc.StringConcatHelper
static long prepend(long indexCoder, byte[] buf, Object value) {
if (value instanceof String) {
return prepend(indexCoder, buf, (String) value); // 字符串:整体拷贝
} else if (value instanceof Boolean) {
return prepend(indexCoder, buf, (boolean) value); // 基础类型:专用路径
} else if (value instanceof Integer) {
return prepend(indexCoder, buf, (int) value);
}
... // Long / Float / Double / Character / char[] / 通用 toString
}
// 字符串版:从 indexCoder 指示的位置向前写入
static long prepend(long indexCoder, byte[] buf, String value) {
indexCoder -= value.length(); // ① 计算起始位置
value.getBytes(buf, (int) indexCoder, LATIN1); // ② 写入
return indexCoder; // ③ 返回新的写入位置
}- 预分配
byte[]:newArray(indexCoder)按总长度分配一次。 - 反向填充:从数组末尾往前写(
indexCoder递减),天然避免正序填充需要的前移操作。 - 避免
StringBuilder扩容开销:StringBuilder追加时若容量不足会Arrays.copyOf扩容(新数组 + 全量拷贝),而精确分配 + 反向填充只有一次写入。 indexCoder是个复合值:高位是已写入长度,低 3 位是编码标识(LATIN1/UTF16),一次寄存器传递同时携带"写到哪 + 什么编码"两个信息。
⑥ StringConcatHelper.mix 系列方法
java
// 累加长度:计算字符串化后的字符数(不同数据类型走不同换算)
static long mixLen(long lengthCoder, Object value) {
if (value instanceof String) {
return lengthCoder + ((String) value).length();
} else if (value instanceof Boolean) {
return lengthCoder + 4; // "true"/"false"
} else if (value instanceof Integer) {
return lengthCoder + stringSize((int) value);
}
...
}
// 合并编码:任一参数是 UTF16(非 ASCII)则整体升级为 UTF16
static long mixCoder(long lengthCoder, Object value) {
long coder = coder(value); // LATIN1=0 / UTF16=1
return lengthCoder | coder; // 编码信息并入低 3 位
}mixLen在分配数组前遍历所有参数累加最终长度,是"精确分配"的前提;stringSize对整数做位运算快速估算位数(String.stringSize的实现)。mixCoder把每个参数的编码标识按位或进indexCoder的低位:只要有一个参数含非拉丁字符,整个拼接结果就必须用UTF16编码,否则全链路保持LATIN1(紧凑、省一半内存)。- 这两个方法都是
static纯函数,JIT 完全内联后拼接的核心循环只是几次算术与内存拷贝。
⑦ 常量子串折叠优化
- 编译期常量折叠:
"a" + "b"这类全部为常量(或编译期常量变量)的表达式,javac 在编译阶段直接算出"ab"放入常量池,不生成任何拼接字节码。 - 常量与非常量混合(如
"count=" + i)则走invokedynamic,其中常量部分嵌入 recipe(\u0001之外的文本),运行时不再拼接常量。 - JIT 内联:
invokedynamic调用点解析后是ConstantCallSite,指向一个可内联的MethodHandle链;热点代码中整个拼接被内联成几条byte[]写指令与一次String构造,无对象分配、无方法调用开销。 - 实战意义:高并发日志、协议编解码中的字符串拼接,通过
-XX:+PrintStringConcat等参数可观测调用点形态;日常编码则无需手工拼StringBuilder——invokedynamic策略已足够快。
总结
| 层面 | 机制 | 收益 |
|---|---|---|
| 编译期 | 常量折叠 + invokedynamic 调用点 | 全常量拼接零开销 |
| 引导期 | StringConcatFactory 策略选择 | 策略可随 JVM 演进 |
| 运行时 | MH_INLINE_SIZED_EXACT + StringConcatHelper | 精确分配、反向填充、无扩容 |
| 优化期 | ConstantCallSite + JIT 内联 | 热路径近似零成本 |
字符串拼接优化的本质是把"分配时机"和"写入方式"做到最优:先 mixLen/mixCoder 精确算出最终大小与编码,再一次性 newArray 反向 prepend,最后 newString 收尾。相比 JDK 8 的 StringBuilder 链式追加,省掉了可能发生的多次扩容拷贝与中间对象,这正是 JEP 280 之后 Java 拼接性能的整体提升来源。