# 9. 如何避免 ArrayList 扩容带来的性能开销?

# 标准答案

ArrayList 扩容会导致 内存复制和性能损耗,最佳策略是 预估元素数量手动初始化容量,避免 频繁触发扩容。此外,使用 ensureCapacity() 提前扩展容量,可减少扩容次数,提高性能。对于 大规模数据存储,考虑 LinkedList其他数据结构 避免动态扩容的影响。

# 答案解析

# ArrayList 扩容的影响

ArrayList 底层是 动态数组,扩容涉及 创建新数组拷贝旧数据,会导致 CPU 计算、GC 压力、内存碎片化 等问题。

# JDK 18 扩容源码分析

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍扩容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
1
2
3
4
5
6
7
8
9

关键点解析

  • 扩容策略:每次扩容为 原容量的 1.5 倍,减少扩容次数但避免过大浪费内存
  • 触发条件:当 size == capacity 时触发扩容
  • 数据迁移Arrays.copyOf() 需要 O(n) 复制成本,影响性能

# 扩容的性能影响

CPU 计算:数组拷贝操作 O(n),影响吞吐量
GC 负担:旧数组 被回收,大数组对象可能触发 Full GC
内存碎片化:频繁创建新数组,会导致 堆内存碎片化,降低分配效率
缓存失效:数组拷贝破坏 CPU 缓存局部性,影响访问性能

# 如何优化扩容问题?

# 1. 提前分配合适容量 (ArrayList(int initialCapacity))

如果可预估数据量,应 直接指定初始容量,避免自动扩容

List<Integer> list = new ArrayList<>(10000); // 避免频繁扩容
1

适用于:

  • 批量插入 场景,如 数据库查询结果存储日志批量处理
  • 需要 高吞吐高并发系统,减少扩容导致的阻塞

# 2. 使用 ensureCapacity() 手动扩容

ArrayList 需要 批量添加,可以 提前扩容,减少扩容开销

ArrayList<Integer> list = new ArrayList<>(10);
list.ensureCapacity(10000); // 手动扩容,避免多次自动扩容
1
2

适用于:

  • 文件解析、大数据处理,预知最大数据量时可手动扩容
  • 网络数据缓存,避免动态扩容影响 RT(Response Time)

# 3. 采用 LinkedListArrayDeque 替代

对于 频繁插入删除 的场景,考虑 LinkedListArrayDeque

Deque<Integer> queue = new ArrayDeque<>(10000); // 预分配容量
1

适用于:

  • 队列(Queue)/栈(Stack) 场景,如 任务调度、消息队列
  • 流式数据处理,避免数组复制影响性能

# 4. 批量添加时,使用 addAll() 代替单次 add()

批量添加元素时,addAll() 比循环 add() 性能更优,因为可 减少扩容次数

List<Integer> list = new ArrayList<>(data.size());
list.addAll(data); // 一次性扩容
1
2

适用于:

  • 数据库查询,避免 for 循环 add() 触发多次扩容
  • 日志批量分析,一次性加载大量数据

# 5. 适用 Collections.synchronizedList(new ArrayList<>()) 替代 Vector

Vector同步的 ArrayList,但它的 synchronized 影响性能,推荐用

List<Integer> syncList = Collections.synchronizedList(new ArrayList<>(10000));
1

适用于:

  • 多线程读多写少场景,如 缓存数据
  • 高并发数据处理,避免 Vector 影响性能

# 适用场景对比

指定初始容量:预知数据量,无需扩容
ensureCapacity():批量添加数据,预扩容
LinkedList:频繁插入删除,不会扩容,但查询慢
ArrayDeque:队列 / 栈,不会扩容
addAll():批量插入,只扩容一次

# 深入追问

为什么 ArrayList 扩容是 1.5 倍,而不是 2 倍?

  • 节省内存:2 倍扩容会浪费过多空间,而 1.5 倍在 减少扩容次数控制内存开销 之间取得平衡
  • 兼容 CPU 缓存:较小的扩容步长减少 缓存失效(cache miss),提升 CPU 访问效率

为什么 LinkedList 没有扩容问题?

  • LinkedList 采用 链表存储,每次插入只修改 指针,不会触发 内存复制
  • LinkedList 查询性能差,不适用于 随机访问 场景

扩容导致 GC 问题如何优化?

  • 手动调用 System.gc() 无效,建议使用 大对象分区(Large Object Space) 避免频繁 GC
  • 避免扩容导致的内存抖动,如日志缓存可采用 环形数组(Ring Buffer) 设计

# 相关面试题

如何减少 ArrayList 的扩容次数?
ArrayList.ensureCapacity(int capacity) 如何优化性能?
LinkedList 为什么不需要扩容?它的缺点是什么?
扩容时,为什么 Arrays.copyOf() 采用 System.arraycopy() 而不是 for 复制?
ArrayListArrayDeque 在高并发下如何选择?