定位ArrayList频繁扩容引发性能抖动,需结合GC日志高频Allocation Failure、JFR中grow/copyOf分配事件突增、async-profiler中Arrays.copyOf CPU占比超5%等运行时指标;高风险场景包括循环内新建空列表、批量添加未预设容量、无效ensureCapacity调用;Arthas可动态watch grow行为验证扩容模式;优化后应观察P99/P999响应毛刺消失及GC暂停波动收窄。

排查 ArrayList 因未预估容量引发的性能抖动,核心是定位“频繁扩容”这一行为本身——它会触发大量 Arrays.copyOf 调用,造成 CPU 突增、GC 压力升高、响应延迟波动。关键不在于看代码有没有 new ArrayList(),而在于观察运行时是否发生了密集、小步长的数组复制。
从 JVM 运行时指标入手抓异常扩容
扩容本质是内存分配 + 数组拷贝,会在以下维度留下明显痕迹:
- GC 日志中出现高频的“Allocation Failure”和短生命周期大对象:每次扩容都新建一个更大的数组(如从 10→15→22→33…),若初始为空且快速添加数百元素,会生成多个中等大小(KB 级)的 Object[],被 G1 或 ZGC 快速回收,日志中表现为密集的 Young GC;
-
JFR(Java Flight Recorder)中
jdk.ObjectAllocationInNewTLAB事件突增,且分配栈深度浅、集中在ArrayList.grow或Arrays.copyOf:开启 JFR 录制 60 秒,筛选 allocation 事件,按 stack trace 分组,若 top 几名全是grow()→copyOf()→add(),基本可确认; -
线程 CPU 样本中
Arrays.copyOf占比异常高(>5%):用async-profiler采样 CPU,火焰图里若该方法频繁出现在 add 调用链底部,说明扩容已成为热点。
通过代码静态扫描识别高风险点
不是所有 new ArrayList() 都有问题,但以下模式大概率导致抖动:
-
循环内反复创建空 ArrayList:例如在 for 循环或 stream map 中写
list.add(...); return new ArrayList(list),每次调用都从 0 开始扩容; -
批量构建场景未指定容量:如 DAO 层查出 5000 条记录后
new ArrayList()再逐条add,实际会扩容约 12 次(10→15→22→33→49→73→109→163→244→366→549→823→1234→1851→2776→4164); -
构造后立即调用
ensureCapacity(0)或无效预估:比如预估为 1,但实际加 1000 个元素,毫无作用。
用 Arthas 动态观测正在发生的扩容
无需重启,直接在线验证某次请求是否触发了非预期扩容:
立即学习“Java免费学习笔记(深入)”;
- 执行
watch java.util.ArrayList grow '{params,returnObj}' -n 5,观察每次扩容前的minCapacity和计算出的newCapacity;若发现连续几次minCapacity=11,12,13...且newCapacity每次只微增(如 15→15→15→22),说明是“边加边扩”的低效模式; - 配合
trace java.util.ArrayList add查看单次请求中add被调用了多少次、耗时分布,若末尾几次 add 耗时陡增(因触发 grow),就是扩容抖动的直接证据。
验证优化效果的简单压测法
改完代码后,不要只看平均 RT,重点看 P99/P999 的毛刺:
- 用 JMeter 或 wrk 对同一接口施加稳定 QPS(如 100/s),持续 2 分钟;
- 对比优化前后 响应时间直方图:未优化时,常有少量请求耗时突然跳到 50–200ms(扩容拷贝阻塞线程);优化后,99% 请求应落在窄带宽内(如 2–8ms);
- 同时监控 JVM 的
gc.pause和thread.count,抖动消失后,这两项的峰值波动幅度应显著收窄。



















