bigywater.tech
Java 内部原理 · 15

字符串 + 拼接编译成了什么:真实反汇编发现不是 new StringBuilder

"a + b 会被编译器自动优化成 new StringBuilder().append(a).append(b)"是几乎每本 Java 教材都写过的知识点。用 javap 真实反编译字节码,看看现在的 JDK 到底生成了什么。

invokedynamic
真实反编译发现:Java 9 之后 + 拼接编译成一条 invokedynamic 指令,字节码里根本没有 new StringBuilder
约 400 倍
循环里 s = s + i 真实计时比显式 StringBuilder.append 慢约 400 倍——编译产物变了,但性能建议真实没变

上一篇:异常真实有多贵:关掉栈追踪只快了 1.3 倍,不是传说中的 10 倍

真实反汇编:a + b + c 编译成了什么

用 javac 编译,再用 javap -c 反汇编,直接看编译器真实生成的字节码,不猜。

ConcatDemo.java(测试代码) static String concatPlus(String a, String b, int c) { return a + b + c; } static String concatBuilder(String a, String b, int c) { return new StringBuilder().append(a).append(b).append(c).toString(); }
真实实测 · javap -c ConcatDemo.class
static java.lang.String concatPlus(java.lang.String, java.lang.String, int);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokedynamic #7,  0   // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String;
       8: areturn

static java.lang.String concatBuilder(java.lang.String, java.lang.String, int);
    Code:
       0: new           #11     // class java/lang/StringBuilder
       3: dup
       4: invokespecial #13     // Method java/lang/StringBuilder."<init>":()V
       7: aload_0
       8: invokevirtual #14     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      ...
      19: invokevirtual #21     // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: areturn

真实反汇编结果很直接:a + b + c 这行代码编译出来只有 4 条指令,核心是一条 invokedynamic,调用的是 makeConcatWithConstants——字节码层面完全没有出现 new StringBuilder。反倒是我自己手写的 concatBuilder 方法,才是真实生成 new StringBuilder 指令的那一个。课本里教的"+ 拼接会被优化成 StringBuilder"这个说法,对 Java 8 及更早版本是真实成立的,但 Java 9 引入 invokedynamic 字符串拼接(JEP 280)之后,这个具体的编译细节已经不再成立了。

真实反汇编:循环里的 + 拼接呢

另一个常见说法是"循环体里每次 + 拼接都会 new 一个 StringBuilder"——反编译循环代码看看是不是这样。

ConcatDemo.java(循环拼接) static String concatLoop(int n) { String s = ""; for (int i = 0; i < n; i++) { s = s + i; } return s; }
真实实测 · javap -c 循环体部分
   5: iload_2
   6: iload_0
   7: if_icmpge     24
  10: aload_1
  11: iload_2
  12: invokedynamic #27,  0   // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
  17: astore_1
  18: iinc          2, 1
  21: goto          5
真实确认:循环体里每一轮也是一条 invokedynamic,同样没有 new StringBuilder 字样——但这不代表循环里用 + 拼接没代价,只是代价换了个不那么直观的形式(见下面的真实计时)

真实验证:即便不是 new StringBuilder,循环里用 + 依然慢得多

字节码层面看不到 StringBuilder 了,但每一轮 invokedynamic 调用背后,makeConcatWithConstants 的引导方法依然要为这次拼接分配一段新内存、拷贝旧内容——这部分开销字节码反汇编看不出来,得靠真实计时。

真实实测 · n=60000,循环 + 拼接 vs 显式 StringBuilder
n = 60000, 两种结果长度相同 = true
循环里 s = s + i   耗时 = 737ms
显式 StringBuilder 耗时 = 1ms
倍数 = 401.2

第二次独立运行倍数是 410.2,结果稳定。原因不是"每次多 new 一个 StringBuilder 对象"这种字节码层面能直接看到的东西,而是每一轮 makeConcatWithConstants 引导方法内部依然要按当前拼接结果的长度重新分配一块内存、把旧字符串内容整体搬过去——这是 O(n) 级别的字符串本身固有的不可变特性决定的,和它编译成什么指令无关。显式的 StringBuilder 是复用同一个底层数组一路 append,不存在这个重复搬运的问题。

所以"别在循环里用 + 拼接"这条建议还成立吗?成立,而且真实数据(400 倍)比很多人想象得还要夸张——只是不能再用"每次都 new 一个 StringBuilder"这个已经过时的理由去解释它了。真实原因是字符串不可变性本身,不是某个具体的对象分配细节。

分步演示:一个 + 号,字节码里到底发生了什么

每一行都是真实反编译或真实运行这段代码得到的结果。

JDK 9 起 + 拼接默认真实编译成 invokedynamic,而不是教程里常说的 StringBuilder.append() 链——真实反编译对比两种策略
这一步新验证的字节码结构

    参考与说明

    • 本文反汇编和计时数据均在本机真实运行 Java 17(javac/javap 默认目标版本)得到;Java 8 及更早版本上重新跑这个实验,预计会看到真实的 new StringBuilder 字节码,因为 invokedynamic 拼接是 Java 9 才引入的。
    • 和StringBuilder 扩容那篇算是同一个主题的两个角度:一个看扩容策略,一个看编译产物。
    • 没有涉及:makeConcatWithConstants 引导方法内部具体怎么根据参数类型选择拼接策略(StringConcatFactory 的实现细节)——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号