ArrayList扩容慢是因为每次扩容需O(n)时间复制元素,插入1万元素未设初容会触发约12次扩容并产生大量临时数组,加剧GC压力;设初容可避免扩容。

ArrayList扩容为什么慢?一次add可能触发多次复制
ArrayList底层是数组,容量固定;一旦add()时发现size + 1 > elementData.length,就必须扩容。默认从10开始,每次按1.5倍增长(10→15→22→33→49…),每次扩容都要调用Arrays.copyOf()把老数组所有元素拷到新数组——这是纯CPU密集型操作,时间复杂度O(n)。
- 插入1万个元素,未设初容:约经历12次扩容,产生12个临时数组对象,GC压力陡增
- 设初容为10000:零扩容,无拷贝,内存连续,缓存命中率更高
- JDK 1.7+优化了“懒初始化”:无参构造
new ArrayList()初始数组长度为0,第一次add()才分配10个槽位,但该优化不改变后续扩容成本
HashMap初始容量怎么算?别直接写16或100
HashMap不是简单“够用就行”,它的性能强依赖哈希分布质量。默认负载因子0.75,意味着容量×0.75就是触发扩容的阈值。如果预估存1000个键值对,直接传1000会出问题——构造器会把它向上取整到最近的2的幂(如1000→1024),但1024 × 0.75 = 768,刚插769个就扩容,白浪费空间。
- 正确公式:
(int) Math.ceil(expectedSize / 0.75)→ 1000 / 0.75 ≈ 1333.33 → 向上取整得1334 - 再由HashMap内部自动转为2的幂:1334 → 实际初始化容量为2048(2¹¹)
- 验证:2048 × 0.75 = 1536 > 1000,确保全程不扩容
- 反例:传
100,实际变128;传1000,实际变1024 → 都会导致过早扩容
哪些场景必须算容量?哪些可以忽略?
不是所有集合都要精打细算,关键看是否满足“可预估 + 高频创建 + 批量写入”三要素。
- 必须算:批量读DB后封装
List(已知查出5000条)、消息队列消费端聚合事件、定时任务汇总日志记录 - 可以忽略:工具类里临时拼接几个字符串的
ArrayList、单次请求中只存2–3个配置项的HashMap - 容易踩坑:在for循环内反复新建小集合(如每个订单建一个
new ArrayList()),看似小,但QPS高时minor GC频率飙升 - 并发场景额外注意:即使容量设对,
HashMap仍线程不安全,高并发put可能引发死循环(JDK 7)或数据丢失(JDK 8+),该换ConcurrentHashMap就换
扩容不只是速度问题,它悄悄吃掉你的GC和内存
频繁扩容产生的旧数组(如10→15→22…中的10、15、22这些byte[])生命周期极短,全进年轻代,很快触发minor GC。更危险的是大集合:ArrayList存大量String或DTO,扩容生成的大数组可能直接晋升老年代,诱发Full GC。
立即学习“Java免费学习笔记(深入)”;
- 一个10MB的
byte[]扩容一次,就多一个10MB临时对象,哪怕只活几毫秒,也加重GC扫描负担 - 内存碎片风险:连续多次扩容导致堆内存中散布多个大小不一的短期数组,降低内存利用率
- 真实案例:某支付系统在大促时young GC耗时从5ms涨到80ms,定位发现是日志聚合模块未设
ArrayList初容,单次请求生成30+个未预估集合
容量不是玄学参数,它是你对数据规模最基础的承诺。算错一次影响不大,但批量、高频、长期运行的服务里,漏掉这个细节,就像在高速路上一直开双闪——不致命,但让所有优化都事倍功半。











