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.)

Author: Peter Mortensen, 2016-02-09

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 OutOfMemoryErrors.

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.

 97
Author: Alex - GlassEditor.com,
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.

 80
Author: Tagir Valeev,
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