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

Horrendous performance & large heap footprint of Java 8 constructor 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);
      }
    }
  }
}

运行 带有 newMethod=true; 的方法将导致该方法在索引达到 30k 后立即失败并显示 OutOfMemoryError。使用 newMethod=false; 程序不会失败,但会一直运行直到被杀死(索引很容易达到 150 万)。

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

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

在第一种情况下 (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 才分配数组。

computeIfAbsent 签名如下:

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

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

Function<Integer, List<Integer>> mappingFunction

当你在需要 Function<Integer, List<Integer>> 的地方写 ArrayList::new 时,编译器会寻找合适的构造函数,即:

public ArrayList(int initialCapacity)

所以基本上你的代码等同于

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

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

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