问题

我刚刚在我们的生产环境中遇到了相当不愉快的经历,导致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[]元素,导致它快速?

(顺便说一下 - 当集合类型为HashSet时也会发生。)


#1 热门回答(93 赞)

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

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

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之前未分配数组。


#2 热门回答(75 赞)

ThecomputeIfAbsent签名如下:

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

所以mappingFunction是接受一个参数的函数。在你的案例K = IntegerV = List<Integer>中,签名变为(省略PECS):

Function<Integer, List<Integer>> mappingFunction

当你在需要2887191651的地方写ArrayList::new时,编译器会寻找合适的构造函数:

public ArrayList(int initialCapacity)

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

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

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

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


原文链接