111 votes

Horrible performance & empreinte de gros tas de référence constructeur Java 8 ?

J'ai juste eu une expérience plutôt désagréable dans notre environnement de production, provoquant OutOfMemoryErrors: heapspace..

J'ai tracé le problème à mon utilisation de l' ArrayList::new dans une fonction.

Pour vérifier que c'est effectivement pire que la normale création via une déclaration du constructeur (t -> new ArrayList<>()), j'ai écrit cette petite méthode:

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);
      }
    }
  }
}

L'exécution de la méthode avec l' newMethod=true; sera la cause de la méthode de l'échec d' OutOfMemoryError juste après l'indice de frappe 30k. Avec newMethod=false; le programme ne manque pas, mais garde la pilonnant jusqu'à ce que mort (indice facilement atteint 1,5 million).

Pourquoi est - ArrayList::new créent un grand nombre Object[] éléments sur le tas qu'il provoque OutOfMemoryError si vite?

(En passant, il se produit également lors de la collecte est de type HashSet.)

97voto

Alex Points 1092

Dans le premier cas (ArrayList::new) vous utilisez le constructeur qui prend une capacité initiale de l'argument, dans le second cas, vous n'êtes pas. Une grande capacité initiale (index dans votre code) provoque un grand Object[] à être affecté, entraînant un OutOfMemoryErrors.

Voici les deux constructeurs implémentations actuelles:

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;
}

Quelque chose de semblable se produit en HashSet, à l'exception de la matrice n'est pas alloué jusqu' add est appelé.

80voto

Tagir Valeev Points 14218

L' computeIfAbsent signature est la suivante:

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

Si l' mappingFunction est la fonction qui reçoit un argument. Dans votre cas K = Integer et V = List<Integer>, de sorte que la signature devient (en omettant PECS):

Function<Integer, List<Integer>> mappingFunction

Lorsque vous écrivez ArrayList::new dans l'endroit où l' Function<Integer, List<Integer>> est nécessaire, le compilateur recherche pour le constructeur approprié qui est:

public ArrayList(int initialCapacity)

Donc, essentiellement, votre code est équivalent à

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

Et vos clés sont traités comme des initialCapacity valeurs qui conduit à la pré-allocation des tableaux de toujours croissante de la taille, ce qui, bien sûr, très vite conduit à l' OutOfMemoryError.

Dans ce cas particulier références constructeur ne sont pas adaptés. L'utilisation des expressions lambda à la place. Ont été l' Supplier<? extends V> utilisés en computeIfAbsent, alors ArrayList::new serait approprié.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X