Java 8 hashmap高内存使用率

我使用散列图存储QTable来实现强化学习算法。 我的hashmap应该存储15000000个条目。 当我运行算法时,我看到进程使用的内存超过1000000K。 当我计算内存时,我预计它的使用量不会超过530000K。 我试着写一个例子,我得到了相同的高内存使用率:

public static void main(String[] args) { HashMap map = new HashMap(16_000_000, 1); for(int i = 0; i < 15_000_000; i++){ map.put(i, i); } } 

我的记忆力:

每个入口集都是32个字节
容量为15000000
HashMap实例使用:32 * SIZE + 4 * CAPACITY memory =(15000000 * 32 + 15000000 * 4)/ 1024 = 527343.75K

我的记忆计算错在哪里?

好吧,在最好的情况下,我们假设字大小为32位/ 4字节(使用CompressedOops和CompressedClassesPointers)。 然后,映射条目由两个单词JVM开销(klass指针和标记字),键,值,哈希码和下一个指针组成,总共6个字,换句话说,24个字节。 因此,拥有15,000,000个条目实例将消耗360 MB。

此外,还有包含条目的数组。 HashMap使用的幂是2的幂,因此对于15,000,000个条目,数组大小至少为16,777,216,消耗64 MiB。

然后,您有30,000,000个Integer实例。 问题是map.put(i, i)执行两个装箱操作,虽然鼓励JVM在装箱时重复使用对象,但不需要这样做,并且在您可能完成的简单程序中不会重复使用在优化器干扰之前。

确切地说,前128个Integer实例被重用,因为对于-128 … +127范围内的值,共享是强制性的,但实现是通过在第一次使用时初始化整个缓存来实现的,因此对于前128次迭代,它不会创建两个实例,但缓存由256实例组成,这个数字是该实例的两倍,因此我们最终再次使用30,000,000个Integer实例。 Integer实例至少包含两个JVM特定字和实际int值,它们将产生12个字节,但由于默认对齐,实际消耗的内存将为16个字节,可分为8个字节。

因此,30,000,000个创建的Integer实例消耗480 MB。

这使得总共360 MB + 64 MiB + 480 MB,超过900 MB,使得1 GB的堆大小完全合理。

但这就是分析工具的用途。 运行你的程序后,我得到了

使用的内存按大小排序

请注意,此工具仅报告对象的已使用大小,即Integer对象的12个字节,而不考虑在查看JVM分配的总内存时将注意到的填充。

我和你有同样的要求……所以决定把我的想法放在这里。

1)有一个很好的工具: jol 。

2)数组也是对象,java中的每个对象都有两个额外的标题:mark和klass,通常大小为4和8字节(这可以通过压缩指针进行调整,但不会详细介绍)。

3)重要的是要注意地图的负载因子(因为它会影响内部数组的大小)。 这是一个例子:

 HashMap map = new HashMap<>(16, 1); for (int i = 0; i < 13; ++i) { map.put(i, i); } System.out.println(GraphLayout.parseInstance(map).toFootprint()); HashMap 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 

看看第二种情况下的大小是多大,因为后备arrays是两倍大(32个条目)。 您只能在16个大小的数组中放入12个条目,因为默认的加载因子是0.75:16 * 0.75 = 12。

为何144? 这里的数学很简单:数组是一个对象,因此:标题为8 + 4个字节。 加上参考的32 * 4 = 140字节。 由于8字节的内存对齐,填充有4个字节,总共144个字节。

4)条目存储在映射内的节点或TreeNode内(节点为32字节,TreeNode为56字节)。 当您使用ONLY整数时,您将只有节点,因为不应该有哈希冲突。 可能存在冲突,但这并不意味着某个数组条目将转换为TreeNode,有一个阈值。 我们可以很容易地certificate只有节点:

  public static void main(String[] args) { Map> map = IntStream.range(0, 15_000_000).boxed() .collect(Collectors.groupingBy(WillThereBeTreeNodes::hash)); // WillThereBeTreeNodes - current class name 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 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) 

让我们从底部开始。

hashmap足迹的总大小为: 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; 

我们有15_000_000个条目,但HashMap的内部数组是两个大小的幂,即每个4字节的16,777,216个引用。

 16_777_216 * 4 = 67_108_864 + 12 bytes header + 4 padding = 67108880