集合中的内存消耗

3

你好,最近在一次面试中,我被问到有一个Hashmap、一个ArrayList和一个Hashset。它们各自包含相同的10个用户定义对象(例如:Employee类对象)。哪个会占用更多堆空间,为什么?

我回答说Hashmap会占用更多的空间,因为它存储键-值对。但是Hashset内部也使用Hashmap来存储值。

  1. 请问有人能给出带有原因的答案吗?
  2. 是否有任何工具或Eclipse插件可以让我自己检查这个问题?

谢谢。


你的答案是正确的。任何文本编辑器都可以查看 JDK 自带的 HashMap 和 HashSet 代码。 - JB Nizet
@JBNizet 但是HashSet也存储为键值对(尽管在这种情况下值是常量)。所以它应该只是HashMap。对吗?因为我可以有任何比hashset的默认PRESENT值更大的值。 - IllegalSkillsException
我不明白你所说的“它应该只是哈希映射”,或者“我可以有任何比默认PRESENT值更大的值”的意思。 - JB Nizet
我所指的是,假设在哈希映射中,我有一个字符串作为值,它占用了300个字节。现在,在哈希集中,PRESENT是一个虚拟值(假设其大小为100个字节)。因此,PRESENT的大小每次都是恒定的,但哈希映射中值的大小可能会有所不同。我希望这次我表达清楚了。 - IllegalSkillsException
2个回答

4
如果你在计算容器和“10个用户定义的对象”所需的内存,那么你是正确的。HashMap会占用更多的空间。尽管HashSet由HashMap支持,但它存储在所有条目中的值都是对同一个虚拟实例的引用。因此,HashSet需要10个键引用+10个值引用+10个元素+1个虚拟实例。另一方面,HashMap需要10个键引用+10个值引用+10个键实例+10个值实例(假设“10个用户定义的对象”存储为值)。当然,要更准确,您还必须计算包含HashMap桶的数组的大小,但这在HashMap和HashSet中是相同的,因为它们包含相同数量的元素。请注意,正如JB Nizet的评论所述,如果HashMap的键是“10个用户定义的对象”的属性,则“10个键实例”不需要额外的内存(因为它们已经存在),因此HashMap和HashSet存储10个对象所需的内存相同,实际上HashSet需要更多的内存,因为它持有对HashMap的引用。ArrayList应该比HashSet和HashMap占用更少的内存,因为ArrayList的后备数组具有默认的初始长度为10(足以存储10个对象),而HashMap的桶数组具有默认的初始长度为16(也足以存储10个对象,假定我们使用默认的负载因子0.75)。

1
鉴于他们仅仅说员工被存储在地图中,可以假设(我这么做了)键是员工的一个字段(例如姓名或ID)。在这种情况下,没有额外的10个键实例,HashMap和HashSet消耗相同数量的内存(实际上HashSet需要对其后备HashMap的引用,因此稍微多一些)。 - JB Nizet
@JBNizet 如果这个键是“用户定义对象”的一个属性,那么你说得对。好观点。 - Eran
2
由于问题涉及十个元素,我不会期望任何容量增加操作。ArrayList的默认容量为10,因此所有元素都适合其中,HashMap的默认容量为16,负载因子为0.75,因此在增加容量之前它将接受多达12个元素。当然,在这种情况下(以及其他任何情况下),16仍然比10大,因此HashMap/HashSet在这种情况下具有更大的容量(并且在需要时HashMap会将容量加倍,而ArrayList使用1.5的因子)。 - Holger
@Eran 我很喜欢你的答案,但我真的需要证明,所以我加了一个小的补充... - Eugene

2

我发现这很有趣,虽然我同意Eran的观点,但需要适当的证明。我正在使用JOL进行测量

为了举例说明,我创建了一个Employee,它有两个字段:String nameint age

那么让我们看看发生了什么:

List<Employee> list = new ArrayList<>();
list.add(new Employee(22, "a"));

System.out.println(GraphLayout.parseInstance(list).totalSize()); //152 bytes

让我们看看这个空格是从哪里来的:

12 bytes ArrayList headers
4 bytes int modCount in ArrayList
4 bytes int size in ArrayList
4 bytes for the reference "elementData" in ArrayList

12 bytes for the Employee headers
4 bytes int age Employee
4 bytes for String name reference 
4 bytes padding (objects are 8 bytes aligned)

12 bytes for the String "a" headers
4 bytes for the char[] reference 
4 bytes for the int hash
4 bytes padding

12 bytes for the new char[] { 'a' }
4 bytes the size of the array (store in headers)
2 bytes for char 'a'
6 bytes padding

40 bytes for the 10 references in elementData array
12 bytes for it's headers (arrays are Objects)
4 bytes for the size (arrays have a size)

为了举例说明,我要添加2个员工,并缩短有关大小的解释:
HashMap<Employee, Integer> map = new HashMap<>();
map.put(new Employee(22, "a"), 100);
map.put(new Employee(23, "b"), 200);

System.out.println(GraphLayout.parseInstance(map).toFootprint()); 

你将会得到这样的输出:
  COUNT       AVG       SUM   DESCRIPTION
     2        24        48   [C
     1        80        80   [Ljava.util.HashMap$Node;
     2        16        32   java.lang.Integer
     2        24        48   java.lang.String
     1        48        48   java.util.HashMap
     2        32        64   java.util.HashMap$Node
     2        24        48   org.erabii.tenelemdiff.Test$Employee
    12                 368   (total)

总大小为368字节。现在让我们将它们放入一个HashSet中:

HashSet<Employee> set = new HashSet<>();
set.add(new Employee(22, "a"));
set.add(new Employee(23, "b"));

System.out.println(GraphLayout.parseInstance(set).totalSize()); // 368 bytes

您可以看到,在这种特定情况下,HashSetHashMap的大小相同。如果添加了超过12个条目(默认情况下HashMap重新调整大小),情况可能会变得更加棘手,并且潜在地会将其存储桶从LinkedNode更改为TreeNode,差异相当显著,请在此处阅读更多信息。一个Node是32字节,而一个TreeNode是56字节。

不错的分析!但是,你得到的HashSetHashMap都是368字节的结果不是巧合吗?毕竟,你的HashMap示例有两个额外的Integer实例,但另一方面,HashSet示例有额外的private transient HashMap<E,Object> map;引用(以及同时拥有HashSetHashMap实例的开销)。如果你添加/放置10个条目/元素,你会得到相同的结果吗? - Eran
@Eran 很好的发现 - 我 故意 选择这些值来产生相同的字节。是的,对于10个条目,这些值将会不同。 - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接