【问题标题】:Horrendous performance & large heap footprint of Java 8 constructor reference?Java 8 构造函数参考的可怕性能和大堆占用?
【发布时间】:2016-05-19 17:09:11
【问题描述】:

刚刚在我们的生产环境中遇到了一次比较不愉快的经历,导致OutOfMemoryErrors: heapspace..

我将此问题追溯到我在函数中使用ArrayList::new

为了验证这实际上比通过声明的构造函数 (t -> new ArrayList<>()) 进行的正常创建执行得更差,我编写了以下小方法:

public class TestMain {
  public static void main(String[] args) {
    boolean newMethod = false;
    Map<Integer,List<Integer>> map = new HashMap<>();
    int index = 0;

    while(true){
      if (newMethod) {
        map.computeIfAbsent(index, ArrayList::new).add(index);
     } else {
        map.computeIfAbsent(index, i->new ArrayList<>()).add(index);
      }
      if (index++ % 100 == 0) {
        System.out.println("Reached index "+index);
      }
    }
  }
}

使用newMethod=true; 运行该方法将导致该方法在索引达到30k 后以OutOfMemoryError 失败。使用 newMethod=false; 时,程序不会失败,而是会继续运行直到被杀死(索引轻松达到 150 万)。

为什么ArrayList::new 会在堆上创建这么多Object[] 元素,导致OutOfMemoryError 如此之快?

(顺便说一句 - 当集合类型为 HashSet 时也会发生这种情况。)

【问题讨论】:

  • 一个微妙的。 +1 表示容易绊倒的东西...

标签: java constructor java-8 out-of-memory method-reference


【解决方案1】:

在第一种情况下(ArrayList::new),您使用的是带有初始容量参数的constructor,而在第二种情况下,您不是。较大的初始容量(您的代码中的index)会导致分配较大的Object[],从而导致您的OutOfMemoryErrors。

这是两个构造函数的当前实现:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

HashSet 中会发生类似的情况,只是在调用add 之前不会分配数组。

【讨论】:

  • @Durandal:Map.computeIfAbsent 方法在第一种情况下将index 变量的值发送到构造函数,从而创建一个大的 bakcing 数组。在第二种情况下,使用了零参数构造函数,它根本不创建支持数组。
  • 啊,所以索引实际上变成了初始大小。就这么解决了,谢谢。
  • @resueman:不,Java 8 版本的默认构造函数在添加第一个元素之前不会创建支持数组。见stackoverflow.com/a/34250231/2711488
  • 感谢您的回答 - 我根本没有想到它会从函数中的方法中获取参数并推断出由于它是一个整数,它也应该用作参数在构造函数中。我认为使用构造函数引用总是会采用空构造函数。感谢您的教育!
【解决方案2】:

computeIfAbsent 签名如下:

V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

所以mappingFunction接收一个参数的函数。在您的情况下为K = IntegerV = List&lt;Integer&gt;,因此签名变为(省略PECS):

Function<Integer, List<Integer>> mappingFunction

当您在需要Function&lt;Integer, List&lt;Integer&gt;&gt; 的地方写ArrayList::new 时,编译器会寻找合适的构造函数:

public ArrayList(int initialCapacity)

所以基本上你的代码相当于

map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);

并且您的键被视为initialCapacity 值,这会导致预分配大小不断增加的数组,当然,这会很快导致OutOfMemoryError

在这种特殊情况下,构造函数引用不合适。请改用 lambda。如果Supplier&lt;? extends V&gt; 用于computeIfAbsent,那么ArrayList::new 将是合适的。

【讨论】:

  • 感谢您的详尽回答 - 认为亚历克斯是第一位的,所以他的解决方案得到了赞扬 :)
  • @AndersK,没问题。我只是想用不同的词来解释相同的内容(可能对某些人来说我的答案更清楚)。
  • 哈!现在我知道微软的“属”从哪里得到它们是 MVC 语法。
猜你喜欢
  • 1970-01-01
  • 2020-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-12
  • 2018-12-25
相关资源
最近更新 更多