初始容量应设为大于等于预期元素数除以0.75后向上取整到最近的2的幂;否则频繁resize导致O(n)开销、GC压力及并发性能问题。

初始容量太小会触发频繁 resize,拖慢插入性能
HashMap 底层是数组 + 链表/红黑树,initialCapacity 决定了初始桶(bucket)数量。默认是 16,负载因子 0.75f,意味着存到第 12 个元素就会触发第一次扩容——整个哈希表重建、所有已有 key 重新 hash、再散列到新数组。这个过程不是 O(1),而是 O(n),且伴随大量内存分配和 GC 压力。
- 预期存 1000 个键值对却用
new HashMap():大概率经历 4–5 次 resize(16 → 32 → 64 → 128 → 256 → 512),每次都要复制全部已有元素 - 高并发批量导入场景下,多个线程同时触发 resize,JDK 7 可能链表成环死循环;JDK 8 虽修复了成环,但竞争仍导致 CPU 尖刺和吞吐骤降
- 监控中可观察到
sun.misc.Unsafe.copyMemory或HashMap.resize()方法耗时突增,GC 日志出现密集的 Young GC
怎么算出“刚好够用”的初始容量?别直接除以 0.75
公式看似简单:initialCapacity = (int) Math.ceil(expectedSize / 0.75f),但 HashMap 构造函数内部会把它**向上取整到最近的 2 的幂**。比如你传 134,实际容量变成 256(因为 128 );传 100,结果还是 128。
所以更稳妥的做法是:先算理论值,再手动对齐 2 的幂,避免无谓浪费:
int expectedSize = 1000; int capacity = (int) Math.ceil(expectedSize / 0.75f); // ≈ 1334 capacity = tableSizeFor(capacity); // JDK 内部方法逻辑:返回 ≥ capacity 的最小 2 的幂 → 2048Map
map = new HashMap<>(capacity); 立即学习“Java免费学习笔记(深入)”;
如果你不想手写 tableSizeFor,直接用 JDK 提供的静态辅助也行(Java 8+):
import java.util.HashMap;// 等效于 HashMap 内部逻辑 static int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= 1 << 30) ? 1 << 30 : n + 1; }
初始容量设太大,真会浪费内存吗?看场景
是的,但浪费程度取决于数据规模和 JVM 设置。一个空的 HashMap 容量为 2048,底层数组就是 Node[2048],每个 Node 引用占 4 字节(32 位 JVM)或 8 字节(64 位 + 普通对象指针压缩关闭),仅数组本身就要 ~16KB(8×2048)。如果只存几十个元素,纯属冗余。
- 缓存类场景(如用户 session 映射):通常有明确上限(如 1w 用户),建议按需计算,不盲目堆大容量
- 临时聚合容器(如 stream.collect(Collectors.toMap(...))):若上游 size 可预估,显式传 capacity;否则依赖框架默认行为更安全
- 注意:容量过大不会影响读性能(get 仍是 O(1) 平均),但会略微拉长迭代(如 keySet().forEach)时间,因要遍历更多 null 桶
JDK 7 和 JDK 8 在容量初始化上有什么关键差异?
核心逻辑一致:都向上取整到 2 的幂,都用 threshold = capacity × loadFactor 控制扩容时机。但底层实现细节影响实操判断:
- JDK 7:
resize()是单线程全量拷贝,扩容期间 map 不可用;多线程 put 可能引发死循环(链表反转成环) - JDK 8:
resize()支持并发协助迁移(helpTransfer),扩容过程 map 仍可读写;链表转红黑树阈值为 8,缓解高冲突下的退化问题 - 但二者对「初始容量设置不当」的后果描述完全一致:都是性能瓶颈主因之一,阿里 Java 开发手册、Google Guava 文档、Spring 源码注释都明确建议「预估 size 后显式指定」
真正容易被忽略的是:即使用了 JDK 8,如果在构造时传了非 2 的幂数字(如 new HashMap(100)),它仍会默默变成 128,而开发者可能误以为“我只申请了 100 个桶”。查 capacity 最可靠的方式是反射调用 capacity() 方法,而不是依赖传入值。











