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