我有和你类似的需求,所以决定在这里分享我的想法。
1) 有一个非常好的工具可以实现这个功能:jol。
2) 数组也是对象,在Java中每个对象都有两个额外的头部:标记和类,通常为4和8字节大小(这可以通过压缩指针进行调整,但不会进入细节)。
3) 这里需要注意地图的负载因子(因为它影响内部数组的调整大小)。以下是一个示例:
HashMap<Integer, Integer> map = new HashMap<>(16, 1);
for (int i = 0; i < 13; ++i) {
map.put(i, i);
}
System.out.println(GraphLayout.parseInstance(map).toFootprint());
HashMap<Integer, Integer> map2 = new HashMap<>(16);
for (int i = 0; i < 13; ++i) {
map2.put(i, i);
}
System.out.println(GraphLayout.parseInstance(map2).toFootprint());
这个的输出结果不同(只显示相关行):
1 80 80 [Ljava.util.HashMap$Node; // first case
1 144 144 [Ljava.util.HashMap$Node; // second case
看第二种情况,由于后备数组的大小是两倍大 (32个条目),因此大小比第一种情况要大。你只能在16大小的数组中放置12个条目,因为默认负载因子为0.75:16 * 0.75 = 12。
为什么是144? 数学很简单:一个数组是一个对象,因此:8+4字节用于头部。加上32 * 4字节用于引用 = 140字节。由于内存对齐为8字节,还有4字节用于填充,总共为144字节。
4) 条目被存储在映射中的Node或TreeNode中(Node为32字节,TreeNode为56字节)。由于只使用整数,所以只会有Nodes,因为不应该有哈希冲突。可能会有冲突,但这并不意味着某个数组条目将被转换为TreeNode,因为有一个阈值。我们可以轻松地证明只会有Nodes:
public static void main(String[] args) {
Map<Integer, List<Integer>> map = IntStream.range(0, 15_000_000).boxed()
.collect(Collectors.groupingBy(WillThereBeTreeNodes::hash));
System.out.println(map.size());
}
private static int hash(Integer key) {
int h = 0;
return (h = key.hashCode()) ^ h >>> 16;
}
这将得到15_000_000的结果,没有合并,因此也没有哈希冲突。
5) 当您创建Integer对象时,会有一个对象池(范围从-127到128-这也可以调整,但为了简单起见,我们不要调整)。
6) Integer是一个对象,因此它有12字节的头和4字节的实际int值。
考虑到这一点,让我们尝试查看15_000_000个条目的输出(由于您使用负载因子为1,因此无需创建内部容量为16_000_000)。这需要很长时间,请耐心等待。我还给了它一个
-Xmx12G和-Xms12G
HashMap<Integer, Integer> map = new HashMap<>(15_000_000, 1);
for (int i = 0; i < 15_000_000; ++i) {
map.put(i, i);
}
System.out.println(GraphLayout.parseInstance(map).toFootprint());
以下是jol说的话:
java.util.HashMap@9629756d footprint:
COUNT AVG SUM DESCRIPTION
1 67108880 67108880 [Ljava.util.HashMap$Node;
29999872 16 479997952 java.lang.Integer
1 48 48 java.util.HashMap
15000000 32 480000000 java.util.HashMap$Node
44999874 1027106880 (total)
让我们从底层开始。
哈希表的总占用空间大小为:1027106880字节或1,027 MB。
节点实例是每个条目所在的包装类。它的大小为32字节;由于有1500万个条目,因此有以下行:
15000000 32 480000000 java.util.HashMap$Node
为什么是32字节?它存储了哈希码(4字节)、键引用(4字节)、值引用(4字节)、下一个节点引用(4字节)、12字节的头部和4字节的填充,总共32字节。
1 48 48 java.util.HashMap
一个HashMap实例 - 48字节用于其内部。
如果你真的想知道为什么是48字节:
System.out.println(ClassLayout.parseClass(HashMap.class).toPrintable());
java.util.HashMap object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 Set AbstractMap.keySet N/A
16 4 Collection AbstractMap.values N/A
20 4 int HashMap.size N/A
24 4 int HashMap.modCount N/A
28 4 int HashMap.threshold N/A
32 4 float HashMap.loadFactor N/A
36 4 Node[] HashMap.table N/A
40 4 Set HashMap.entrySet N/A
44 4 (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
接下来是整数实例:
29999872 16 479997952 java.lang.Integer
有3000万个整数对象(减去缓存在池中的128个)
1 67108880 67108880 [Ljava.util.HashMap$Node;
我们有1500万个条目,但HashMap的内部数组大小是2的幂次方,也就是16,777,216个4字节引用。
16_777_216 * 4 = 67_108_864 + 12 bytes header + 4 padding = 67108880
HashMap
中存储的对象所需的内存:30,000,000个Integer
对象(15,000,000个键,15,000,000个值)需要额外的240 MB。 - Thomas Kläger