stringbuilder扩容时因system.arraycopy拷贝旧数组内容导致性能开销,触发条件为count+新增字符数>capacity,扩容公式为newcapacity=oldcapacity×2+2,预设容量可避免拷贝。

StringBuilder扩容时会触发数组拷贝,这是其性能开销的主要来源——不是每次append都慢,而是扩容那一刻代价集中。
扩容触发条件很明确
StringBuilder内部维护一个char[]数组和一个记录当前长度的count字段。当执行append等修改操作时,如果新内容写入后会超出当前数组容量(即 count + 新增字符数 > capacity),就会触发扩容。
- 默认初始容量是16,空构造器创建的对象第一次扩容就到34(16×2+2)
- 后续扩容公式为:newCapacity = oldCapacity × 2 + 2,直到足够容纳新数据
- 扩容不是“加16”,也不是按需精确分配,而是成倍增长,避免频繁重分配
数组拷贝是真正的性能瓶颈
扩容本身不耗时,真正花时间的是System.arraycopy()把旧数组内容复制到新数组。这个操作是O(n)时间复杂度,且涉及内存分配与连续块拷贝。
- 拷贝量取决于当前已存字符数(count),不是容量(capacity)
- 例如:已有3000个字符,数组容量3200,append 300个字符 → 触发扩容 → 拷贝3000个char → 分配新数组 → 再写入300个
- 若提前预估容量(如new StringBuilder(5000)),可完全规避这次拷贝
如何观察和验证扩容行为
可通过反射或JDK9+的Unsafe方式读取StringBuilder的value字段(char[])和capacity,但更实用的是用JMH做微基准测试对比。
- 写两个测试:一个用默认构造,一个指定足够容量,相同字符串拼接逻辑
- 差异显著时(尤其在循环拼接场景),主要归因于数组拷贝次数减少
- 配合-XX:+PrintGCDetails或JFR可发现大量临时char[]对象分配,侧面印证扩容频次
实际编码中的优化建议
多数情况下无需过度优化,但在高频拼接、确定长度范围或批量处理文本时,预设容量能带来稳定收益。
- 知道大致长度:new StringBuilder(expectedLength)
- 拼接固定集合(如List
):先sum所有字符串length,再+少量余量 - 避免在循环内反复创建StringBuilder:复用实例比每次都new更省
- 超大文本拼接(MB级):考虑StringJoiner、BufferedWriter或流式处理,而非单个StringBuilder










