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
.)
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 OutOfMemoryError
s.
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é.
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é.