【问题标题】:Why is this hashmap with initial capacity trying to resize?为什么这个具有初始容量的哈希图试图调整大小?
【发布时间】:2018-01-14 06:23:58
【问题描述】:

为什么这段代码会抛出异常?

public static void main(String[] args) {
    Map<Integer, Integer> map = new HashMap<>(Integer.MAX_VALUE);
    System.out.println("map size: "+map.size());
    map.put(1, 1);
    System.out.println("map size: "+map.size());
}

输出:

map size: 0
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:703)
    at java.util.HashMap.putVal(HashMap.java:628)
    at java.util.HashMap.put(HashMap.java:611)
    at com.fredcrs.codejam.NumberToBinary.main(NumberToBinary.java:24)

hashmap 是否应该只在满时才调整大小?

编辑: 它在初始化时也会抛出相同的异常:

Map<Integer, Integer> map = new HashMap<>(Integer.MAX_VALUE-3);

【问题讨论】:

  • 但是你告诉它有一个 MAX_VALUE 的initialCapacity 请参阅javadocs
  • 是的,但初始容量是指用于索引键的数组的大小(散列)

标签: java dictionary hashmap initialization hashtable


【解决方案1】:

在 Oracle Java 8 JDK 中,在添加元素之前不会分配 HashMap 的存储空间。

如有疑问,只需检查实现 - 您甚至可以在调试器中单步执行。

现代 JDK HashMap 实现在插入第一个元素之前实际上不会分配底层数组,即使您指定了显式大小。比如我的JDK 8版本,构造函数代码如下:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

请注意,没有分配数组。此外,您请求的大小大于我系统上的MAXIMUM_CAPACITY 230,因此实际请求的大小(存储在this.threshold 中为described here)的上限为@ 987654327@。

然后,当你真正去分配数组时,实现会尝试创建一个请求大小的数组。最终,在HashMap.resize() 的深处,有一些逻辑检测到您已达到“最大容量”(因为您要求以最大容量的初始大小开始),并将底层数组的大小设置为Integer.MAX_VALUE

    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }

这随后会分配一个包含 231-1 个int 元素的数组,这需要至少 8G 的堆空间。这就是你得到 OOME 的原因。当我使用-Xmx9G 运行时,它会成功完成输出:

map size: 0
map size: 1

【讨论】:

【解决方案2】:
new HashMap<>(Integer.MAX_VALUE);

您要求的初始数组大小为 231-1 个元素,即 2,147,483,647。每个元素 8 个字节(引用为 64 位)大约是 16 GB 的内存。

除非您有 18GB 左右的可用空间供堆使用,否则您总是会收到 OOM 错误。

您请求了 16GB 的阵列内存,除非该内存可用,否则它将失败。它是否在实例化或第一次插入时失败是一个实现细节。在过去的某个时候,它会在实例化时失败。最近,代码被更改为等到第一次插入。这种改变是可能的,因为何时分配数组的细节不是任何外部合同的一部分——即它没有在 JavaDoc 中提及。

【讨论】:

  • 好的,所以我的猜测是,当我创建 hashmap 对象时,它不会分配数组......它只在添加第一个元素时才这样做
  • 每次元素为put时,都需要检查容量并可能增加容量,所以我猜作者最初解码时并未分配内存
  • @fredcrs - HashMap 最初用于分配数组,但现在它在第一次插入时分配,当您有许多空映射时,这是一个非常合理且强大的优化。
  • @JimGarrison 实际最大尺寸为MAXIMUM_CAPACITY = 1 &lt;&lt; 30;那是 2 pow 30 != Integer.MAX_VALUE
  • @JimGarrison - 由于填充和额外的 mark 字(见此:stackoverflow.com/questions/41314160/…) - 空间可以更快地“吃掉”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-18
  • 1970-01-01
  • 2015-08-03
  • 2014-02-23
  • 2012-10-26
  • 2017-01-11
  • 1970-01-01
相关资源
最近更新 更多