.class文件中字符串拼接用StringBuilder指令:纯字面量如"a"+"b"编译期合并为ldc,含变量如s1+s2则生成new StringBuilder、append、toString指令,后者创建的String对象在堆中而非常量池,故s3=="ab"与s4=s1+s2结果不等。

怎么看 class 文件里字符串拼接到底用了什么指令
直接看 .class 文件反编译后的字节码,就能确认编译器是否做了优化、有没有生成 StringBuilder。关键不是“会不会拼”,而是“谁在拼、在哪拼”。javap -c 是最轻量也最可靠的手段。
常见错误现象:用 IDE 看源码觉得 “s1 + s2” 很简单,但没意识到它背后是对象创建 + 方法调用;或者误以为所有 + 都等价于常量池查找。
- 纯字面量拼接(如
"a" + "b")→ 编译期直接合并为"ab",字节码里只出现一次ldc指令,不会新建对象 - 含变量的拼接(如
s1 + "b"或s1 + s2)→ 字节码必然出现new java/lang/StringBuilder、invokespecial、append、toString - 注意
toString()调用的是StringBuilder.toString(),它内部 new 了一个String对象,且不进常量池(除非显式调用intern())
为什么 s3 == s4 返回 false,但 javap 能一眼看出原因
因为 == 比的是引用地址,而地址来源由字节码决定。只要看到 ldc #7 和 new StringBuilder 同时存在,就说明一个来自常量池、一个来自堆。
使用场景:排查线上 == 判断失效、单元测试断言失败、或面试被问“为什么两个内容相同的 String 不相等”。
-
s3 = "ab"→ 字节码是ldc #4,指向常量池中已驻留的"ab"对象 -
s4 = s1 + s2(其中s1="a",s2="b")→ 字节码含new、append、toString,最终对象在堆中分配 - JDK 版本影响很小:从 JDK 5 开始,含变量的
+就固定转成StringBuilder;JDK 9+ 的byte[]底层不影响这个行为逻辑
容易被忽略的细节:toString() 创建的 String 真的不在常量池里
很多人以为 “拼出来是 "ab",那它就应该和字面量 "ab" 是同一个对象”。错。这个误解源于没看清 StringBuilder.toString() 的实现。
它的源码本质是 return new String(value, 0, count) —— 显式 new,不经过 ldc,也不触发常量池检查。
- 验证方式:对拼接结果调用
.intern()后再比较,s4.intern() == s3才为 true - 性能影响:频繁拼接后又调
intern()可能引发哈希表扩容(StringTableSize默认 60013,但高并发下仍可能冲突) - 兼容性注意:JDK 7+ 常量池在堆中,
intern()是安全的;JDK 6 及以前在永久代,容易OutOfMemoryError: PermGen space
实战建议:别靠猜,用 javap 快速定位问题根因
遇到字符串比较异常、内存占用突增、或想确认某段拼接是否可优化,第一反应不应该是改代码,而是看字节码。它比任何文档都真实。
操作步骤极简:
- 写好测试类(比如含
s1+s2的方法),javac Test.java - 执行
javap -c Test,搜StringBuilder或ldc - 对照行号,定位到具体方法,观察指令序列是否符合预期
真正复杂的地方从来不是“怎么写”,而是“运行时实际发生了什么”。而字节码,就是 JVM 给你的唯一无歧义的回答。

















