Java HashMap 扩容机制详解
一、触发条件
HashMap 在以下两种情况下触发扩容:
-
元素数量超过阈值
阈值(threshold
)= 当前容量(capacity
) × 负载因子(loadFactor
,默认0.75)。
例如:容量为16时,阈值=12,当元素数量超过12时触发扩容。 -
链表转红黑树时的容量不足
当链表长度≥8且当前数组长度<64时,优先扩容而非树化。
二、扩容流程
-
计算新容量
- 默认扩容为原容量的 2倍(
newCap = oldCap << 1
)。 - 若原容量为0(首次初始化),默认初始容量为16。
- 若超过最大容量限制(
MAXIMUM_CAPACITY = 1<<30
),则不再扩容。
- 默认扩容为原容量的 2倍(
-
创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
-
数据迁移(Rehash)
遍历旧数组中的每个桶(bucket),重新计算元素在新数组中的位置:- 链表迁移:JDK8优化为高低位拆分(避免全链表遍历)。
- 红黑树迁移:若树节点数≤6,退化为链表。
-
更新阈值
threshold = (int)(newCap * loadFactor);
三、源码关键逻辑(JDK17)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1. 计算新容量和阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新容量 = 原容量 * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阈值也翻倍
} else if (oldThr > 0) // 初始容量由阈值决定(构造函数指定)
newCap = oldThr;
else { // 默认初始化(容量16,阈值12)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 2. 处理新阈值计算(如原数组为空且阈值未计算)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 3. 创建新数组并迁移数据
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 清理旧桶
if (e.next == null) // 单节点直接迁移
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) // 红黑树迁移
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表高低位拆分优化
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断高位是否为0(是否需要迁移到新位置)
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 低位链表留在原索引位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位链表迁移到新索引(原索引 + oldCap)
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
四、扩容优化(JDK8+)
-
高低位拆分(Bitwise Optimization)
通过(e.hash & oldCap) == 0
判断元素是否留在原位置或迁移到新位置(原索引 + oldCap)。- 原理:新容量为旧容量2倍,哈希值高位决定是否需要位移。
- 优势:避免重新计算哈希,仅用位运算即可确定新位置,时间复杂度降为O(n)而非O(n²)。
-
红黑树迁移优化
- 若树节点数≤6,退化为链表。
- 否则拆分树结构,减少迁移后的树高度。
五、性能影响与优化建议
场景 | 性能影响 | 优化建议 |
---|---|---|
频繁触发扩容 | 数据复制耗时,引发GC压力 | 初始化时指定合理容量(如new HashMap<>(1024) ) |
超大容量初始化 | 内存浪费(数组预分配) | 动态扩容或使用trimToSize() |
负载因子设置过高 | 哈希碰撞概率增加,查询效率降低 | 根据业务调整(如0.6提高查询性能) |
示例:初始化容量计算
若预计存储1000个元素,负载因子0.75:
int initialCapacity = (int) Math.ceil(1000 / 0.75); // 1334 → 2048(最近的2^n)
Map<String, Object> map = new HashMap<>(2048);
六、常见问题
-
为什么默认负载因子是0.75?
- 平衡时间(查询效率)与空间(数组利用率)的折中结果。
- 数学推导表明,在泊松分布下,0.75时哈希碰撞概率较低。
-
扩容是否线程安全?
- HashMap非线程安全,并发扩容可能导致数据丢失或死循环(JDK7及之前版本)。
-
如何避免多次扩容?
- 初始化时预估容量(
initialCapacity = expectedSize / loadFactor + 1
)。
- 初始化时预估容量(
七、对比 Hashtable 扩容
特性 | HashMap | Hashtable |
---|---|---|
扩容倍数 | 2倍 | 2倍+1(奇容量减少哈希碰撞) |
触发条件 | size > threshold | size >= threshold |
线程安全 | 非安全(需外部同步) | 安全(但性能差) |
总结
HashMap的扩容机制通过2倍动态扩容和高低位拆分优化,平衡了哈希碰撞概率与内存利用率。理解其实现细节有助于:
- 避免频繁扩容带来的性能损耗
- 合理初始化容量和负载因子
- 在并发场景中选择
ConcurrentHashMap
替代 - 优化哈希函数减少碰撞概率
通过源码分析和实际场景结合,能够更精准地优化HashMap的使用,提升系统性能。