向 ArrayList 存入百万级元素本身不直接触发 GC,但默认扩容机制导致约20+次数组复制与废弃大数组产生,加剧Eden区压力、提升Young GC频率并可能诱发Full GC;预设容量可使Young GC次数降80%以上、Full GC几乎消失。

向 ArrayList 存入百万级元素本身不会直接触发 GC,但频繁扩容引发的数组复制和旧数组遗弃,会显著加剧堆内存压力,间接导致 GC 频次上升、停顿延长,甚至诱发 Full GC。
扩容过程如何制造 GC 压力
ArrayList 底层是 Object[] 数组,初始容量为 10。每插入一个元素,若 size ≥ capacity,就调用 grow() 方法扩容——新数组长度 ≈ 原容量 × 1.5,再通过 Arrays.copyOf() 将全部旧元素逐个复制过去。
- 每次扩容都要分配一块更大的连续堆内存(如从 1.5MB → 2.25MB)
- 旧数组失去所有引用,立刻变为“不可达对象”,等待下次 Young GC 回收
- 对百万数据,默认策略下约经历 20+ 次扩容,产生 20+ 个待回收的废弃大数组
- 这些大数组集中在 Eden 区,容易快速填满,迫使 GC 提前启动
未预设容量时的真实 GC 行为
以默认构造 new ArrayList() 插入 100 万个 Integer 为例:
- 前几次扩容影响小,但后期单次复制操作涉及数十万对象,CPU 和内存带宽占用高
- 大量短命大数组涌入 Eden 区,Young GC 频率可能从几秒一次升至数百毫秒一次
- 部分未及时回收的大数组可能因 Survivor 区空间不足而“幸存”进入老年代(Premature Promotion)
- 若老年代因此快速填满,将触发代价高昂的 Full GC,造成明显 STW(Stop-The-World)停顿
两种预设容量方式对 GC 的改善效果
关键不是“避免 GC”,而是“让 GC 更少、更轻、更可预测”:
-
构造时指定容量:
new ArrayList(1_000_000)—— 一次性分配足够数组,全程零扩容,仅产生 1 个活跃数组,Eden 区压力极低 -
运行时预声明:
list.ensureCapacity(1_000_000)—— 效果等同于构造时设定,适合无法在创建时确定规模但后续批量写入的场景 - 实测显示:后者可使 Young GC 次数下降 80% 以上,平均 GC 耗时减少 60%,Full GC 几乎消失
配合 JVM 参数进一步稳住内存水位
即使优化了 ArrayList 使用方式,面对持续百万级写入,仍建议同步调整基础内存策略:
- 设置合理堆初始值:
-Xms4g -Xmx4g(避免运行中堆动态扩展带来的额外开销) - 选用低延迟收集器:
-XX:+UseZGC或-XX:+UseG1GC,尤其适合突发性大对象分配 - 监控验证:
jstat -gc观察 YGC 次数、GCT(GC 总耗时)、EU(Eden 使用量)是否显著回落1s










