HashMap的最大容量是多少.

首先, HashMap底层是数组+链表, 所以HashMap的容量约等于 数组长度 * 链表长度.
因为链表长度不固定,甚至可能链表会是树结构, 所以我们主要讨论数组长度.

那么, 数组的最大长度是多长呢? 仔细想想, 好像这么多年也没去看过数组的源码(笑).

1
2
3
4
5
6
7
一是规范隐含的限制。Java数组的length必须是非负的int,
所以它的理论最大值就是java.lang.Integer.MAX_VALUE = 2^31-1 = 2147483647。

二是具体的实现带来的限制。
这会使得实际的JVM不一定能支持上面说的理论上的最大length。
例如说如果有JVM使用uint32_t来记录对象大小的话,那可以允许的最大的数组长度(按元素的个数计算)就会是:
(uint32_t的最大值 - 数组对象的对象头大小) / 数组元素大小

嗯..数组长度理论上可以达到 2^31-1 这么长, 那么HashMap的最大长度也是这么了?

不, 在HashMap中规定HashMap底层数组的元素最大为 1<<30

1
static final int MAXIMUM_CAPACITY = 1 << 30;

为啥呢? 理论上不是可以更长吗?

还记得我们以前提到过的HashMap会把容量定为输入容量的最近的2次幂.

1
2
3
4
5
6
7
8
9
static final 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 >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这串是干嘛呢?

现在想象一个场景, new HashMap(9);
我想初始化一个长度为9的HashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cap: 
00000000 00000000 00000000 00001001

int n = cap - 1:
00000000 00000000 00000000 00001000

n >>> 1:
00000000 00000000 00000000 00000100

n |= n >>> 1:
00000000 00000000 00000000 00001100

n >>> 2:
00000000 00000000 00000000 00000011

n |= n >>> 2:
00000000 00000000 00000000 00001111

n >>> 4:
00000000 00000000 00000000 00001111

n |= n >>> 4:
00000000 00000000 00000000 00001111


n >>> 8:
00000000 00000000 00000000 00001111

n |= n >>> 8:
00000000 00000000 00000000 00001111

n >>> 16:
00000000 00000000 00000000 00001111

n |= n >>> 16:
00000000 00000000 00000000 00001111

这边计算了什么呢

1
00000000 00000000 00000000 00001111 = 15

也就是将原本最高的一位后面全部变成1
也即, 变成了 2^n -1
这样只要最后结果加1, 就会变成离他最近的2次幂.

那这些有什么用呢?

00000000 00000000 00000000 00000001 左边不是31个位置吗? 为什么最大容量不是 1 << 31 ?

如果左移31, 就会变成10000000 00000000 00000000 00000000,
而最高位, 即最左边的位是符号位, 1为负数.

1
2
3
4
5
// 运行这条
System.out.println(0b10000000_00000000_00000000_00000000);

// 输出
-2147483648

数组长度总不能是负数吧. 所以HashMap的数组长度最长是 1<<30


尝试了一下添加1<<30个数进HashMap

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int times = 1<<30;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < times; i++) {
map.put(i, i);
System.out.println(i);
}
}

可以看到, 我没有设置HashMap初始大小, 因此默认大小是16, 因为我们知道, HashMap在一定条件下会扩容, 扩容导致的问题就是数据迁移.

所以在运行到 1486699 的时候第一次出现明显卡顿,时间很短 大概一秒左右, 再往后的输出停顿时间越来越久.

因此小伙伴们如果预先知道要装多少数据, 或者大概数据, 不妨精心计算一下HashMap的初始大小.我认为 总数据量 / (3|4|5|6) 都可以.

因为按照同一节点下链表的数据多少规律, 同一个节点下挂载多个数据的概率是逐渐减少的.(而且没有哪个map会装这么多数据吧

1
2
3
4
5
6
7
8
9
0:    0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006

在 23739181 的时候就OOM了, 或许下次把堆内存调大点再试试(逃