StringBuilder扩容是显式过程:先检查count+新增字符数>当前数组长度则触发,再按newCapacity=(oldCapacity<<1)+2与最小需容量取大者确定新容量,最后分配内存、复制数据、切换引用。

StringBuilder 的底层数组扩容不是自动“悄悄变大”,而是一套明确、可预测、有开销的显式过程:每次扩容都涉及内存分配、数据复制和引用切换。
扩容触发条件很实在
扩容发生在操作前的容量检查阶段,判断依据只有一个:当前已用长度(count)+本次新增字符数 > 当前底层数组长度(value.length)。只要这个不等式成立,append()、insert() 或 replace() 后紧接着的 append() 都会立刻触发扩容。
- 初始容量为 16,调用 append("0123456789abcdef")(16 字符)→ count = 16,刚好不扩容
- 再 append("g") → 16 + 1 = 17 > 16 → 立即扩容
- insert(0, "XYZ") 到一个已有 15 字符的 StringBuilder 中 → 插入位置 0 + 新增 3 字符 = 3,但判断的是 0 + 3 > value.length?不对——实际校验的是插入后总长度是否超限,即 15 + 3 > value.length,所以仍看总需容量
新容量怎么算:公式兜底,不硬套
扩容不是简单翻倍,而是分两步确定最终大小:
- 先按固定公式算基础值:newCapacity = (oldCapacity << 1) + 2(即旧容量 × 2 + 2)
- 再和“最小所需容量”(count + 新增字符数)比较:如果基础值不够,就直接取最小所需容量
- 最后还要确保不超过上限 Integer.MAX_VALUE - 8,超限抛 OutOfMemoryError
例如:当前容量 34,要追加 200 字符 → 最小需容量 = 当前 count + 200。若 count 是 30,则需 230;而 34×2+2 = 70 < 230 → 实际扩容到 230。
立即学习“Java免费学习笔记(深入)”;
扩容背后是三次真实动作
每次扩容都绕不开这三步,且每一步都有可观测开销:
- 在堆上分配一块新的 char[](JDK 9+ 是 byte[]),大小就是上面算出的新容量
- 调用 System.arraycopy() 或 Arrays.copyOf(),把旧数组全部内容逐字拷贝过去(时间复杂度 O(n))
- 将内部 value 引用指向新数组,旧数组变成垃圾,等待 GC 回收
频繁扩容会产生大量短命 char[] 对象,推高年轻代 GC 频率。压测中若发现 Arrays.copyOf 是热点方法,基本就是扩容太勤了。
让扩容更可控的关键做法
目标不是消灭扩容,而是让它发生得少、发生得准:
- 预估初始容量:比如拼接 100 条日志,每条平均 75 字符,加 10% 余量 → new StringBuilder(8300)
- 优先批量追加:用 append("abc") 而非循环 append('a')、append('b')、append('c'),减少扩容触发次数和边界检查开销
-
复用实例:在工具方法或线程内用 ThreadLocal
持有,避免每次新建默认 16 字节数组 - 运行时观察:用 capacity() − length() 查剩余空间,长期接近 0 就该调大初始值;若 capacity() 远大于 length()(如 10000 vs 200),说明初始设大了,白白占堆


















