【Java】深入解析Java字符串常量池与StringBuilder:从内存原理到工程实践

发布时间:2026/7/1 5:46:04

【Java】深入解析Java字符串常量池与StringBuilder:从内存原理到工程实践 Q1StringBuilder的append里写的字符串字面量是存放在字符串常量池里吗是的存放在字符串常量池里。当你写下类似这样的代码时StringBuildersbnewStringBuilder();sb.append(hello);底层发生的具体事情是这样的1. 字符串字面量的存放hello是一个字符串字面量。在Java中所有的字符串字面量在类加载时或编译时都会被放入字符串常量池中。无论你是把它赋值给一个变量还是作为参数传给append()方法它都会在常量池中占据一个位置。2.append()方法的执行过程append()方法接收到这个字符串后并不是把常量池里的引用直接塞进 StringBuilder 里而是做了一个**“拷贝”**操作StringBuilder内部维护着一个char[]数组Java 9 以后改为了byte[]。append(hello)被调用时底层会把hello这个字符串里的字符逐个复制到StringBuilder内部的这个数组里。复制完成后StringBuilder内部的数组扩容或者指针后移与常量池中的hello就没有直接的引用关系了。3. 内存图解概念你可以这样理解它们在内存中的关系字符串常量池存着hello这个唯一的字符串对象。堆内存非池化区域存着StringBuilder对象以及它内部包裹的那个char[]/byte[]数组数组里面复制了h, e, l, l, o这些数据。代码证明我们可以通过一段简单的代码来证明这一点publicclassTest{publicstaticvoidmain(String[]args){StringBuildersbnewStringBuilder();sb.append(hello);// 1. 证明字面量本身在常量池里Strings1hello;System.out.println(s1hello);// 输出 true说明字面量在常量池// 2. 证明 StringBuilder 里的内容不在常量池里Strings2sb.toString();System.out.println(s2hello);// 输出 false// 因为 toString() 是在堆中 new 了一个新的 String 对象并没有去常量池里找}}延伸注意点编译期优化如果你使用 号拼接纯字面量Java 编译器在编译阶段会直接帮你做优化根本不会生成StringBuilder// 你写的代码Stringsabc;// 编译器编译后的效果等同于Stringsabc;这种情况下abc直接就是一个字面量存在于常量池中。但如果是包含变量的拼接编译器就会默默地使用StringBuilder来处理了Stringaa;Stringsabc;// 编译后相当于new StringBuilder().append(a).append(b).append(c).toString();此时b和c依然在常量池里而变量a指向的a也在常量池里StringBuilder只是把它们的内容复制到了自己的内部数组中。Q2写项目代码的时候经常说为了不要过多占用字符串常量池少写一些长的字符串字面量但是有时候一些业务通知语句就是需要长的字符串字面量。这是一个非常经典的问题首先要澄清一个历史遗留的误区1. 破除迷思长字符串字面量真的会“撑爆”常量池吗在现代 JavaJava 7 及以后中完全不需要担心这个问题。远古时代Java 6 及以前字符串常量池放在永久代这块内存非常小且不受 JVM 垃圾回收的严格控制。如果拼命写长字符串字面量确实容易引发OutOfMemoryError: PermGen space。这就是那句“老话”的来源。现代 JavaJava 7 及以后字符串常量池被移到了Java 堆内存中。堆内存动辄几个GB甚至十几个GB。一个 1000 个汉字的字符串占用内存大约 2KB~3KB。即使你的项目里有10000 条这么长的业务通知总共也就占用20MB~30MB的堆内存。对于现代服务器来说这连水花都算不上。而且Java 8 引入了字符串去重功能G1 垃圾回收器下底层会自动优化进一步减少内存占用。结论如果是静态的、固定的长文本直接写在代码里作为字面量在内存上没有任何问题。2. 既然内存不是问题那长字符串真正的问题是什么长业务通知语句写在代码里真正让人头疼的是工程维护问题而不是内存代码丑陋几百个字符挤在一行或者满屏的号拼接严重影响代码可读性。无法动态修改运营想改个活动文案程序员就得改代码、重新编译、打包、发版极其低效。无法国际化如果以后要支持繁体、英文代码里到处都是硬编码的中文改造起来是灾难。包含变量时容易出错比如尊敬的 name 您的订单 orderId 已发货一旦顺序变了或者少了个空格很容易出 Bug。3. 针对长业务通知的最佳实践根据你的实际场景推荐以下几种处理方式方案一如果只是纯静态文本Java 13 语法糖如果你用的 JDK 版本较高且这段话绝对不会有变量直接用文本块清爽又高效// Java 13 引入的 Text Blocks底层依然是常量池里的字符串字面量Stringnotice 尊敬的用户 您好 由于系统升级维护本平台将于2023年XX月XX日00:00至06:00进行停机维护。 届时将无法进行充值、提现等操作。给您带来不便敬请谅解 ;方案二带有变量的长文本使用StringBuilder或String.format如果文本里有变量不要用一堆号显得专业一点// 使用 StringBuilder (你最初问的)StringnoticenewStringBuilder().append(尊敬的 ).append(userName).append(\n).append(您的订单 ).append(orderId).append( 已发货。\n).append(预计到达时间).append(estimateTime).append(。).toString();// 或者使用 String.format (阅读性更好适合变量较少的情况)StringnoticeString.format(尊敬的 %s\n您的订单 %s 已发货。\n预计到达时间%s。,userName,orderId,estimateTime);注这两种方式其中的字面量部分依然会进常量池完全不用有心理负担。方案三需要频繁修改或不发版就改文案配置中心 / 数据库如果是营销短信、活动通知千万别写死在代码里。把模板存到数据库表里或者存到 Apollo / Nacos 等配置中心里。代码里只做“占位符替换”// 从数据库或配置中心读取到的长字符串模板Stringtemplate尊敬的${userName}您抽中了${prize}请于${expireTime}前领取;// 使用工具类替换如 Apache Commons Text 的 StringSubstitutor或者自己写正则替换StringnoticereplacePlaceholder(template,paramMap);方案四复杂的排版模板引擎如果这个通知不仅是长还涉及复杂的排版比如要生成一段 HTML 格式的邮件或者带样式的微信模板消息直接上轻量级模板引擎如 Freemarker、Thymeleaf或者简单的 Velocity!-- 存放在 resources/templates/notice.html --htmlbodyp尊敬的 ${user.name}/pp您的订单strong${order.id}/strong已发货。/p/body/htmlJava 代码只需要传入数据模型渲染即可彻底分离代码和文案。总结怕常量池爆掉不存在现代 Java 随便写。为了性能用 StringBuilder 拼长字面量没必要现代编译器连a b都会自动优化成ab放在常量池。真正该做的根据业务场景静态/动态/需修改/复杂排版选择文本块、格式化、数据库配置或模板引擎一切以**“好维护”**为最高准则。Q3为什么编译器不优化显式的 append语义明确程序员显式写出append编译器认为这是程序员的明确意图它不应该去篡改你的逻辑。编译器很“笨”Java 的编译器javac做的优化非常少它的主要工作是把.java翻译成.class。真正的深度优化是交给 JVM 的JIT即时编译器在运行期做的。JIT 的运行期优化虽然javac没优化但当这段代码被 JVM 执行成千上万次后JIT 编译器可能会进行“方法内联”等优化把append的调用开销打平但这是运行期的行为不是编译期的合并。总结纠正String s a b;-会在编译期优化成ab。sb.append(a).append(b);-绝对不会在编译期优化成sb.append(ab)。再次感谢你的质疑这帮我纠正了一个非常关键的表述漏洞如果你在代码里连续append好多个长字面量它确实会在运行时进行多次数组的复制和扩容检查虽然没有新对象产生但依然有方法调用的开销。不过正如上一个回答所说现代 JVM 的 JIT 优化极其强大这种开销在绝大多数业务场景下依然是可以忽略不计的。

相关新闻