capacity() 返回缓冲区总容量而非已用长度,如 new StringBuilder("abc") 的 capacity() 通常为 16;预估总字符数并指定初始容量(如 new StringBuilder(2048))或用 ensureCapacity() 主动预留可避免频繁扩容影响性能。

capacity() 返回的是当前缓冲区总容量,不是已用长度
很多人误以为 capacity() 表示“当前字符串长度”,其实它返回的是内部 char[] 数组的长度——即已分配但未必填满的空间。比如 new StringBuilder("abc") 的 length() 是 3,但 capacity() 通常是 16(JDK 默认初始容量)。这个差值就是预留的扩容余量。
真正影响性能的是频繁扩容:每次超出当前 capacity(),JVM 就要新建更大数组、复制旧内容。所以预估最终总字符数,再用构造函数指定容量,比依赖默认扩容更可控。
- 默认构造器
new StringBuilder()容量为 16,适合极短字符串;超过就得扩容 - 如果确定最终会拼接约 2000 字符,直接写
new StringBuilder(2048)(向上取整到 2 的幂更友好) -
capacity()在运行时可调用,但只是“查看”,不能直接设——要扩容得靠ensureCapacity(int)
用 ensureCapacity() 主动预留空间,比 let-it-grow 更稳
构建过程中动态估算总长(比如循环拼接 N 条日志,每条平均 L 字符),可以在关键节点调用 ensureCapacity() 提前分配。它不会缩容,只在当前容量不足时扩容到至少参数值。
注意:多次调用 ensureCapacity() 不会重复分配,JVM 内部有判断;但过度预留(如预估 10MB 实际只用 10KB)会浪费堆内存,尤其在高并发短生命周期场景下。
- 推荐在循环前或首次拼接前调用一次,比如
sb.ensureCapacity(expectedTotalLength) - 避免在循环体内反复调用,例如
for (int i = 0; i —— 这样没意义,且增加方法调用开销 - 若无法预估总量(如流式读取未知长度数据),不如用
StringBuffer或考虑分块处理,硬塞 capacity 反而误导
扩容策略和实际内存占用有隐含成本
JDK 的扩容逻辑是:新容量 = 旧容量 × 2 + 2(JDK 11+ 优化为更平滑增长,但仍非线性)。这意味着从 16 → 34 → 70 → 142… 每次扩容都涉及数组拷贝,且新数组可能跨代进入老年代,触发额外 GC。
所以“预估”不是越准越好,而是要留合理余量:太小仍会扩容,太大浪费内存。一个经验法则是:预估长度 × 1.2~1.5,再向上取整到最近的 2 的幂(如 1000 → 1024,3000 → 4096)。
- 查看实际扩容次数?可用 JFR 或
-XX:+PrintGCDetails观察是否频繁触发 young GC,间接反推字符串操作压力 - 某些 JDK 版本对小字符串做了逃逸分析优化,
StringBuilder可能栈上分配——此时capacity()调优收益有限,别过早优化 - 如果最终要转成
String,记得sb.toString()会新建字符数组拷贝,这部分开销无法靠capacity()规避
别忽略 substring 和 delete 导致的 capacity 滞后问题
调用 delete(0, sb.length()) 或 setLength(0) 清空内容后,capacity() 保持不变——缓冲区没释放。下次拼接仍复用原数组,这是设计使然,也是优势(避免反复 new)。但如果你清空后准备拼一个极小字符串(比如几个字符),却还顶着 8192 的 capacity,那内存就闲置了。
这种场景少见,但如果明确知道后续拼接规模骤降,又在意内存 footprint(如中间件长期运行的 buffer 复用),可以主动重建:sb = new StringBuilder(16).append(sb),但这要权衡对象创建成本。
-
setLength(0)最快,保留 capacity;delete(0, len)等价;new StringBuilder()最干净但最重 - 不要依赖
trimToSize()来“缩小”容量——它只把 capacity 收到当前length(),但多数时候 length=0,结果就是 capacity=0,下次 append 又得扩容,反而更差 - 如果用作线程内临时拼接,基本不用管 capacity 滞后;如果是池化复用的 StringBuilder,才需要关注清空后的状态一致性

















