死循环中用+拼接字符串快速OOM的根本原因是高频创建不可变String和StringBuilder对象导致Eden区迅速填满、Minor GC频繁、大量对象晋升老年代直至撑爆。

死循环中用 + 拼接字符串引发 OOM,根本原因不是“内存不够”,而是不可变对象高频创建 + 堆中大量短命对象堆积 + GC 跟不上分配速度。排查和重构需从 JVM 内存行为、字节码机制、对象生命周期三方面切入。
为什么死循环里用 + 会快速 OOM
每次 result += s 都不是简单追加,而是触发一整套隐式操作:
- JVM 将其编译为
new StringBuilder().append(result).append(s).toString() -
toString()创建一个全新String对象(JDK9+ 是byte[],仍占堆) - 旧
result字符串失去引用,但仍在堆中等待回收;新StringBuilder实例也立即丢弃 - 死循环持续执行 → 每轮至少生成 2 个堆对象(1 个
StringBuilder,1 个String)→ Eden 区迅速填满 → 频繁 Minor GC → 大量对象被晋升到老年代 → 最终老年代撑爆,抛java.lang.OutOfMemoryError: Java heap space
如何定位是它导致的 OOM
不靠猜,靠证据:
- 用
jstat -gc <pid>观察:Eden 使用率长期 99%+、YGC 次数暴涨(如每秒几十次)、老年代使用率单向攀升 - 导出堆转储(
jmap -dump:format=b,file=heap.hprof <pid>),用 Eclipse MAT 分析:Top Consumers 中java.lang.String和java.lang.StringBuilder占比极高,且多数对象的retained heap较大 - 反编译问题代码(
javap -c YourClass):确认循环体内确实生成了new StringBuilder和toString调用指令 - 添加 JVM 参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,日志中出现 “GC pause” 频繁且老年代回收失败(Full GC (Ergonomics)后仍 OOM)
用 StringBuilder 正确重构的关键动作
不是简单换类名,而是切断对象爆炸链:
- 实例必须复用:在循环外创建,而非每次迭代 new 一个
-
容量必须预估:构造时传入合理初始容量(例如
list.size() * avgLength * 1.2),避免内部char[]或byte[]多次扩容复制 - 避免 toString() 过早调用:只在真正需要字符串结果时调用一次,不在循环内反复调用
-
注意线程场景:若该 StringBuilder 被多个线程共享,改用
StringBuffer;否则一律用StringBuilder
重构示例:
错误写法(OOM温床):String result = "";<br>while (running) {<br> result += getNextPart(); // 每轮都 new StringBuilder + new String<br>}正确写法(稳定可控):
StringBuilder sb = new StringBuilder(1024); // 预设容量,复用实例<br>while (running) {<br> sb.append(getNextPart()); // 仅修改内部数组<br>}<br>String result = sb.toString(); // 仅最后调用一次额外防御:堵住隐式拼接漏洞
有些 OOM 不在主逻辑,而在日志或调试语句里:
- 检查所有形如
log.info("a=" + a + ", b=" + b)的语句 —— 即使日志级别关闭,拼接仍执行 - 统一替换为 SLF4J 占位符:
log.info("a={}, b={}", a, b),参数只在真正输出时计算和拼接 - 禁用
String.format()处理动态长串,它内部也 new StringBuilder 且无容量控制;必要时手动 new 并预设容量

















