前言

最近闲来无事无意间点进 ArrayList#add()中看了下,产生了几个之前未注意到的困惑点,特此记录下解惑的过程。

  • java.util.ArrayList#grow 中有多处判断容量大小,采用的均是newCapacity - MAX_ARRAY_SIZE > 0为何不直接使用newCapacity < MAX_ARRAY_SIZE这种方式呢?这样不是更加简洁明了吗?
  • java.util.ArrayList#MAX_ARRAY_SIZE 值为何是Integer.MAX_VALUE - 8,直接使用Integer.MAX_VALUE不行吗?
  • java.util.AbstractList#modCount变量有何作用,为什么每次调用都要进行modCount++;操作?

正文

直接比较 与 减法比较

若在常规情况下(不存在溢出)二者是一致的,不会有任何问题。

若由于扩容导致newCapacity溢出变为了负数,则情况会有所不同

  • 直接比较newCapacity > MAX_ARRAY_SIZE
    newCapacity 是负数(如因溢出导致)时,条件为 false,可能跳过超大容量处理逻辑。
  • 减法比较newCapacity - MAX_ARRAY_SIZE > 0
    newCapacity 因溢出变为负数,但 newCapacity - MAX_ARRAY_SIZE 可能因整数溢出变为正数,从而触发超大容量处理逻辑。

通过减法比较,可以 捕获溢出后的异常情况

  • newCapacity 溢出为负数,但实际需要分配的容量极大(如 minCapacity 接近 Integer.MAX_VALUE),newCapacity - MAX_ARRAY_SIZE > 0 会强制进入 hugeCapacity() 方法。
  • hugeCapacity() 中会检查 minCapacity 是否溢出(minCapacity < 0),若溢出则抛出 OutOfMemoryError

MAX_ARRAY_SIZE默认值

避免 JVM 实现限制

数组在内存中分配时,除了存储实际元素外,还需要额外的 元数据 (如对象头信息)。不同 JVM 实现可能在数组对象头中存储以下内容:

  • 类型指针 (Class Metadata Pointer):标识数组类型。
  • 数组长度 (Array Length):记录数组实际大小。
  • 对齐填充 (Padding):满足内存对齐要求。

例如:

  • 在 64 位 JVM 中,对象头可能占用 12~16 字节 (若开启压缩指针则为 12 字节)。
  • 若尝试分配长度为 Integer.MAX_VALUE 的数组,可能因元数据占用空间而导致实际内存不足,触发 OutOfMemoryError

通过将 MAX_ARRAY_SIZE 设为 Integer.MAX_VALUE - 8,为对象头预留空间,减少因 JVM 元数据导致的内存分配失败。

极端情况仍允许突破

hugeCapacity(int minCapacity) 方法中,如果请求的容量超过 MAX_ARRAY_SIZE

java

复制

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
  • minCapacity 超过 MAX_ARRAY_SIZE,仍尝试分配 Integer.MAX_VALUE
  • 若此时 JVM 允许,则分配成功;否则抛出 OutOfMemoryError

这意味着 MAX_ARRAY_SIZE 是一个 安全阈值 ,而 Integer.MAX_VALUE 是最终的硬性上限。

AbstractList#modCount作用

modCount 的本质是集合框架中用于 追踪结构性修改的计数器 ,通过 Fail-Fast 机制在迭代过程中快速暴露并发修改问题。这一设计帮助开发者在调试阶段尽早发现逻辑错误,但需理解其局限性和正确使用场景。

1. 基本概念

  • modCount 是什么?
    modCount(modification count)是一个整型计数器,记录集合发生结构性修改的次数。
    • 结构性修改 :指改变集合大小(如添加、删除元素)或直接改变其内部结构的操作。
    • 非结构性修改 :如仅修改某个元素的值(例如 list.set(0, "new")),不会增加 modCount
  • 源码示例 (以 ArrayList 为例):
public boolean add(E e) {
    modCount++;  // 结构性修改,计数器自增
    add(e, elementData, size);
    return true;
}

2. 核心作用:Fail-Fast 机制

modCountFail-Fast(快速失败) 机制的核心,用于在以下场景快速检测并发修改:

场景 1:单线程迭代时修改集合

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
list.add("D");  // 直接修改集合,modCount++
it.next();       // 迭代时检查 modCount,抛出 ConcurrentModificationException

场景 2:多线程并发修改

List<String> list = new ArrayList<>(Arrays.asList("A", "B"));
new Thread(() -> list.add("C")).start();  // 线程修改集合,modCount++
Iterator<String> it = list.iterator();    // 迭代器保存当前 modCount
it.next();  // 检查 modCount 是否变化,若变化则抛出异常

3. 实现原理

迭代器内部记录 expectedModCount

每个迭代器在创建时,会保存当前集合的 modCountexpectedModCount 字段。在迭代过程中,每次操作前会检查两者是否一致:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

触发异常的条件

  • 若通过集合自身的方法修改结构(如 add()remove()),modCount++,但迭代器的 expectedModCount 未更新。
  • 若通过迭代器自身的方法修改结构(如 Iterator.remove()),会同步更新 expectedModCountmodCount,避免异常。

标题:ArrayList 扩容相关思考
作者:JonLv
地址:http://39.108.183.139:8080/articles/2025/02/28/1740739547493.html