Ужасная производительность и большой объем кучи ссылки на конструктор 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
было бы подходящим.