Java 8构造函数引用的可怕性能和大堆占用空间?

And*_*s K 107 java constructor out-of-memory java-8 method-reference

我刚刚在我们的生产环境中遇到了相当不愉快的经历 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);
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

运行方法newMethod=true;将导致方法OutOfMemoryError在索引达到30k后失败.随着newMethod=false;程序不会失败,但一直冲击直到被杀(索引容易达到150万).

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

(顺便说一下 - 当集合类型出现时也会发生HashSet.)

Ale*_*lex 94

在第一种情况(ArrayList::new)中,您使用的是带有初始容量参数的构造函数,在第二种情况下,您不是.较大的初始容量(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;
}
Run Code Online (Sandbox Code Playgroud)

发生了类似的事情HashSet,除了在add调用之前不分配数组.

  • 啊,所以索引实际上是初始大小.多数民众赞成可以解决问题,谢谢. (8认同)
  • @resueman:不,Java 8版本的默认构造函数在添加第一个元素之前不会创建支持数组.请参见http://stackoverflow.com/a/34250231/2711488 (4认同)
  • @Durandal:`Map.computeIfAbsent`方法在第一种情况下将`index`变量的值发送给构造函数,创建一个大的bakcing数组.在第二种情况下,使用零参数构造函数,它根本不创建后备数组. (2认同)

Tag*_*eev 77

computeIfAbsent签名如下:

V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
Run Code Online (Sandbox Code Playgroud)

所以这mappingFunction接收一个参数的函数.在你的情况K = IntegerV = List<Integer>,所以它的签名变成(省略PECS):

Function<Integer, List<Integer>> mappingFunction
Run Code Online (Sandbox Code Playgroud)

当你ArrayList::newFunction<Integer, List<Integer>>必要的地方写,编译器寻找合适的构造函数,它是:

public ArrayList(int initialCapacity)
Run Code Online (Sandbox Code Playgroud)

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

map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);
Run Code Online (Sandbox Code Playgroud)

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

在这种特殊情况下,构造函数引用不合适.请改用lambdas.如果Supplier<? extends V>用过computeIfAbsent,则ArrayList::new适当.

  • @AndersK,没问题.我只想用不同的词语解释同样的问题(可能对某些人来说,我的答案更清楚). (4认同)