Ужасная производительность и большой объем кучи ссылки на конструктор Java 8?

У меня был довольно неприятный опыт в нашей производственной среде, вызвав 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; приведет к тому, что метод завершится с ошибкой OutOfMemoryError сразу после обращения индекса 30k. С newMethod=false; программа не терпит неудачу, но продолжает ударяться до тех пор, пока не будет убита (индекс легко достигнет 1,5 миллионов).

Почему ArrayList::new создает так много элементов Object[] в куче, что вызывает OutOfMemoryError так быстро?

(Кстати, это также происходит, когда тип коллекции HashSet.)

Ответ 1

В первом случае (ArrayList::new) вы используете конструктор , который принимает аргумент начальной емкости, во втором случае вы этого не делаете. Большая начальная емкость (index в вашем коде) вызывает выделение большого Object[], в результате чего ваш OutOfMemoryError s.

Вот две текущие реализации двух конструкторов:

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

Подпись computeIfAbsent следующая:

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

Итак, mappingFunction - это функция, которая получает один аргумент. В вашем случае K = Integer и V = List<Integer>, поэтому подпись становится (опуская PECS):

Function<Integer, List<Integer>> mappingFunction

Когда вы пишете ArrayList::new в том месте, где Function<Integer, List<Integer>> необходимо, компилятор ищет подходящий конструктор, который:

public ArrayList(int initialCapacity)

Итак, ваш код эквивалентен

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

И ваши ключи рассматриваются как значения initialCapacity, что приводит к предварительному распределению массивов все большего размера, что, конечно, довольно быстро приводит к OutOfMemoryError.

В этом конкретном случае ссылки на конструкторы не подходят. Вместо этого используйте лямбда. Если Supplier<? extends V> использовался в computeIfAbsent, тогда ArrayList::new было бы подходящим.