Horrendous wydajność i duża hałda footprint Java 8 constructor reference?
Po prostu miałem dość nieprzyjemne doświadczenie w naszym środowisku produkcyjnym, powodując OutOfMemoryErrors: heapspace..
Wyśledziłem problem z użyciem ArrayList::new
w funkcji.
Aby zweryfikować, czy to rzeczywiście działa gorzej niż normalne tworzenie za pomocą zadeklarowanego konstruktora (t -> new ArrayList<>()
), napisałem następującą małą metodę:
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);
}
}
}
}
Uruchomienie metody z newMethod=true;
spowoduje, że metoda zawiedzie z OutOfMemoryError
zaraz po trafieniu indeksu 30k. z newMethod=false;
program nie zawiedzie, ale utrzymuje się aż do śmierci (indeks łatwo osiąga 1,5 mln).
Dlaczego ArrayList::new
tworzy tak wiele Object[]
elementów na stercie, że powoduje to OutOfMemoryError
tak szybko?
(przy okazji-dzieje się tak również wtedy, gdy typem zbioru jest HashSet
.)
2 answers
W pierwszym przypadku (ArrayList::new
) używasz konstruktora , który pobiera początkowy argument pojemności, w drugim przypadku nie. Duża pojemność początkowa (index
w Twoim kodzie) powoduje przydzielenie dużej Object[]
, co skutkuje Twoimi OutOfMemoryError
s.
Oto obecne implementacje dwóch konstruktorów:
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;
}
Coś podobnego dzieje się w HashSet
, z tym że tablica nie jest przydzielana dopóki nie zostanie wywołana add
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-16 21:54:24
Podpis computeIfAbsent
jest następujący:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
Zatem mappingFunction
jest funkcją, która otrzymuje jeden argument. W Twoim przypadku K = Integer
i V = List<Integer>
, więc podpis staje się (pomijając PECS):
Function<Integer, List<Integer>> mappingFunction
Kiedy piszesz ArrayList::new
w miejscu gdzie Function<Integer, List<Integer>>
jest konieczne, kompilator szuka odpowiedniego konstruktora, którym jest:
public ArrayList(int initialCapacity)
Więc zasadniczo Twój kod jest równoważny
map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);
I twoje klucze są traktowane jako wartości initialCapacity
, co prowadzi do wstępnej alokacji z tablic o coraz większych rozmiarach, co oczywiście dość szybko prowadzi do OutOfMemoryError
.
W tym konkretnym przypadku odniesienia do konstruktora nie są odpowiednie. Zamiast tego użyj lambda. Gdyby Supplier<? extends V>
użyto w computeIfAbsent
, to ArrayList::new
byłoby właściwe.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-09 16:32:23