我使用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
我的记忆计算错在哪里?
答案 0 :(得分:7)
嗯,在最好的情况下,我们假设字大小为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 :(得分:3)
我和你有着同样的要求......所以决定把我的想法放在这里。
1)有一个很棒的工具:jol。
2)数组也是对象,java中的每个对象都有两个额外的标题:mark和klass,通常大小为4和8字节(这可以通过压缩指针进行调整,但不会详细介绍)。 / p>
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)条目存储在地图内的节点或TreeNode内(节点为32字节,TreeNode为56字节)。当您使用ONLY整数时,您将只有节点,因为不应该有哈希冲突。 可能是冲突,但这并不意味着某个数组条目将转换为TreeNode,有一个阈值。我们可以很容易地证明只有节点:
public static void main(String[] args) {
Map<Integer, List<Integer>> 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<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)
让我们从底部开始。
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