前言
最近闲来无事无意间点进 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 机制
modCount
是 Fail-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
每个迭代器在创建时,会保存当前集合的 modCount
到 expectedModCount
字段。在迭代过程中,每次操作前会检查两者是否一致:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
触发异常的条件
- 若通过集合自身的方法修改结构(如
add()
、remove()
),modCount++
,但迭代器的expectedModCount
未更新。 - 若通过迭代器自身的方法修改结构(如
Iterator.remove()
),会同步更新expectedModCount
和modCount
,避免异常。