web-dev-qa-db-fra.com

Des performances horribles et une grande empreinte de tas de Java 8?

Je viens de vivre 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 ArrayList::new Dans une fonction.

Pour vérifier que cela fonctionne réellement moins bien que la création normale via un constructeur déclaré (t -> new ArrayList<>()), j'ai écrit la petite méthode suivante:

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 newMethod=true; Entraînera l'échec de la méthode avec OutOfMemoryError juste après que l'index atteigne 30k. Avec newMethod=false;, Le programme n'échoue pas, mais continue de battre jusqu'à ce qu'il soit tué (l'indice atteint facilement 1,5 million).

Pourquoi ArrayList::new Crée-t-il autant d'éléments Object[] Sur le tas qu'il provoque OutOfMemoryError si rapidement?

(Soit dit en passant - cela se produit également lorsque le type de collection est HashSet.)

107
Anders K

Dans le premier cas (ArrayList::new) vous utilisez le constructeur qui prend un argument de capacité initial, dans le second cas vous ne l'êtes pas. Une grande capacité initiale (index dans votre code) provoque une grande Object[] à allouer, résultant en vos OutOfMemoryErrors.

Voici les implémentations actuelles des deux constructeurs:

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 similaire se produit dans HashSet, sauf que le tableau n'est pas alloué jusqu'à ce que add soit appelé.

94
Alex

La signature computeIfAbsent est la suivante:

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

Ainsi, le mappingFunction est la fonction qui reçoit un argument. Dans ton cas K = Integer et V = List<Integer>, donc la signature devient (sans PECS):

Function<Integer, List<Integer>> mappingFunction

Lorsque vous écrivez ArrayList::new à l'endroit où Function<Integer, List<Integer>> est nécessaire, le compilateur recherche 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ées comme des valeurs initialCapacity ce qui conduit à une pré-allocation de tableaux de taille toujours croissante, ce qui, bien sûr, conduit assez rapidement à OutOfMemoryError.

Dans ce cas particulier, les références de constructeur ne conviennent pas. Utilisez plutôt des lambdas. Où le Supplier<? extends V> utilisé dans computeIfAbsent, puis ArrayList::new serait approprié.

77
Tagir Valeev