Maison  >  Article  >  Java  >  Comment réduire la surcharge du garbage collection Java

Comment réduire la surcharge du garbage collection Java

PHPz
PHPzavant
2023-05-04 14:31:06650parcourir

Astuce n°1 : Prédire la capacité de la collection

Toutes les collections Java standard, y compris les implémentations personnalisées et étendues (telles que Trove et Guava de Google), utilisent des tableaux (soit des types de données natifs, soit des types basés sur des objets) sous le capot. Parce qu'une fois qu'un tableau est alloué, sa taille est immuable, donc l'ajout d'éléments à la collection entraînera dans la plupart des cas la nécessité de demander à nouveau un nouveau tableau de grande capacité pour remplacer l'ancien tableau (en référence au tableau utilisé par le mise en œuvre sous-jacente de la collection).

Même si une taille pour l'initialisation de la collection n'est pas fournie, la plupart des implémentations de collection tentent d'optimiser le traitement de réallocation du tableau et d'amortir sa surcharge au minimum. Cependant, les meilleurs résultats peuvent être obtenus en fournissant la taille lors de la construction de la collection.

Analysons le code suivant à titre d'exemple simple :

public static List reverse (Liste & lt; ? étend T & gt; liste) {

Résultat de la liste = new ArrayList();

pour (int i = list.size() - 1; i & gt; = 0; i--) {

result.add(list.get(i));

}

résultat de retour ;

}

Cette méthode alloue un nouveau tableau, puis le remplit avec des éléments d'une autre liste, uniquement dans l'ordre inverse.

Cette méthode de traitement peut coûter cher en termes de performances, et le point d'optimisation est la ligne de code qui ajoute des éléments à une nouvelle liste. Au fur et à mesure que chaque élément est ajouté, la liste doit s'assurer que son tableau sous-jacent dispose de suffisamment d'espace pour accueillir le nouvel élément. S'il existe un emplacement libre, le nouvel élément est simplement stocké dans l'emplacement libre suivant. Sinon, un nouveau tableau sous-jacent est alloué, le contenu de l'ancien tableau est copié dans le nouveau tableau et les nouveaux éléments sont ajoutés. Cela entraînera l'allocation de la baie plusieurs fois et les anciennes baies restantes seront éventuellement récupérées par le GC.

Nous pouvons éviter ces allocations redondantes en indiquant à son tableau sous-jacent combien d'éléments il stockera lors de la construction de la collection

public static List reverse (Liste & lt; ? étend T & gt; liste) {

Résultat de la liste = new ArrayList(list.size());

pour (int i = list.size() - 1; i & gt; = 0; i--) {

result.add(list.get(i));

}

résultat de retour ;

}

Le code ci-dessus spécifie un espace suffisamment grand pour stocker les éléments list.size() via le constructeur d'ArrayList et termine l'allocation lors de l'initialisation, ce qui signifie que List n'a pas besoin d'allouer à nouveau de la mémoire pendant le processus d'itération.

La classe collection de Guava va encore plus loin, vous permettant de spécifier explicitement le nombre d'éléments attendus ou de spécifier une valeur prédite lors de l'initialisation de la collection.

1

Résultat 2List = Lists.newArrayListWithCapacity(list.size());

Résultat de la liste = Lists.newArrayListWithExpectedSize(list.size());

Dans le code ci-dessus, le premier est utilisé lorsque nous savons déjà exactement combien d'éléments la collection va stocker, tandis que le second est alloué de manière à prendre en compte les estimations erronées.

Astuce n°2 : Traitez directement le flux de données

Lorsqu'il s'agit de flux de données, tels que la lecture de données à partir d'un fichier ou le téléchargement de données depuis le réseau, le code suivant est très courant :

1byte[] fileData = readFileToByteArray(new File("myfile.txt"));

Le tableau d'octets résultant peut être analysé comme un document XML, un objet JSON ou un message mis en mémoire tampon de protocole, avec certaines options courantes disponibles.

L'approche ci-dessus n'est pas judicieuse lorsqu'il s'agit de fichiers volumineux ou de fichiers de taille imprévisible, car des OutOfMemoryErrors se produiront lorsque la JVM ne pourra pas allouer de tampon pour traiter le fichier réel.

Même si la taille des données est gérable, l'utilisation du modèle ci-dessus entraînera toujours une surcharge énorme en matière de garbage collection, car elle alloue une très grande zone dans le tas pour stocker les données du fichier.

Une meilleure façon de gérer cela consiste à utiliser un InputStream approprié (tel que FileInputStream dans cet exemple) pour le transmettre directement à l'analyseur, au lieu de lire l'intégralité du fichier dans un tableau d'octets en une seule fois. Toutes les bibliothèques open source grand public fournissent des API correspondantes pour accepter directement un flux d'entrée à traiter, telles que :

FileInputStream fis = new FileInputStream(fileName);

MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);

Astuce n°3 : Utilisez des objets immuables

L'immuabilité présente de nombreux avantages. Je n’ai même pas besoin d’entrer dans les détails. Cependant, il existe un avantage qui a un impact sur la collecte des déchets et qui mérite d’être pris en compte.

Les propriétés d'un objet immuable ne peuvent pas être modifiées après la création de l'objet (l'exemple ici utilise les propriétés des types de données de référence), telles que :

Classe publique ObjectPair {

Objet final privé en premier ;

Objet final privé deuxième ;

public ObjectPair (Objet en premier, Objet en second) {

this.first = premier;

this.second = seconde;

}

Objet public getFirst() {

reviens en premier ;

}

Objet public getSecond() {

reviens en deuxième;

}

}

L'instanciation de la classe ci-dessus produira un objet immuable - toutes ses propriétés sont modifiées avec final et ne peuvent pas être modifiées une fois la construction terminée.

L'immuabilité signifie que tous les objets référencés par un conteneur immuable sont créés avant la construction du conteneur. En ce qui concerne GC : le conteneur est au moins aussi jeune que la plus jeune référence qu'il contient. Cela signifie que lors de l'exécution du garbage collection dans la jeune génération, le GC ignore les objets immuables car ils appartiennent à l'ancienne génération et ne termine pas la collecte des objets immuables tant qu'il n'est pas déterminé que ces objets immuables ne sont référencés par aucun objet de la nouvelle génération. ancienne génération. Recycler.

Moins d’objets analysés signifie moins d’analyses de pages mémoire, ce qui signifie des durées de vie GC plus courtes, ce qui signifie des pauses GC plus courtes et un meilleur débit global.

Conseil n°4 : Soyez prudent avec la concaténation de chaînes

Les chaînes sont probablement la structure de données non native la plus couramment utilisée dans toutes les applications basées sur JVM. Cependant, en raison de sa surcharge implicite et de sa facilité d’utilisation, il est très facile de devenir responsable d’une utilisation excessive de mémoire.

Le problème ne vient évidemment pas de la chaîne littérale, mais de l'initialisation de la mémoire allouée au moment de l'exécution. Jetons un coup d'œil rapide à un exemple de construction dynamique d'une chaîne :

public static String toString (tableau T[]) {

Résultat de la chaîne = "[";

pour (int i = 0; i & lt; array.length; i++) {

résultat += (array[i] == array ? "this" : array[i]);

if (i & lt; array.length - 1) {

résultat += ", ";

}

}

résultat += "]";

résultat de retour ;

}

Il s'agit d'une méthode apparemment intéressante qui prend un tableau de caractères et renvoie une chaîne. Mais c'est désastreux pour l'allocation de mémoire des objets.

Il est difficile de voir derrière ce sucre syntaxique, mais la situation réelle en coulisses est la suivante :

public static String toString (tableau T[]) {

Résultat de la chaîne = "[";

pour (int i = 0; i & lt; array.length; i++) {

StringBuilder sb1 = nouveau StringBuilder (résultat);

sb1.append(array[i] == array ? "this" : array[i]);

résultat = sb1.toString();

if (i & lt; array.length - 1) {

StringBuilder sb2 = nouveau StringBuilder (résultat);

sb2.append(", ");

résultat = sb2.toString();

}

}

StringBuilder sb3 = nouveau StringBuilder(résultat);

sb3.append("]");

résultat = sb3.toString();

résultat de retour ;

}

Les chaînes sont immuables, ce qui signifie qu'à chaque fois qu'une concaténation se produit, elles ne sont pas elles-mêmes modifiées, mais de nouvelles chaînes sont allouées à leur tour. De plus, le compilateur utilise la classe StringBuilder standard pour effectuer ces opérations de concaténation. Ceci est problématique car chaque itération alloue implicitement une chaîne temporaire et un objet StringBuilder temporaire pour aider à construire le résultat final.

Le meilleur moyen est d'éviter la situation ci-dessus et d'utiliser StringBuilder et l'ajout direct au lieu de l'opérateur de concaténation natif ("+"). Voici un exemple :

chaîne statique publique toString (tableau T[]) {

StringBuilder sb = new StringBuilder("[");

pour (int i = 0; i & lt; array.length; i++) {

sb.append(array[i] == array ? "this" : array[i]);

if (i & lt; array.length - 1) {

sb.append(", ");

}

}

sb.append("]");

return sb.toString();

}

Ici, nous allouons le seul StringBuilder au début de la méthode. À ce stade, toutes les chaînes et éléments de liste ont été ajoutés à un seul StringBuilder. Enfin, utilisez la méthode toString() pour la convertir en chaîne et la renvoyer en une seule fois.

Astuce n°5 : Utilisez une collection de types natifs spécifiques

La bibliothèque de collections standard de Java est simple et prend en charge les génériques, permettant la liaison semi-statique des types lors de l'utilisation de collections. Par exemple, si vous souhaitez créer un ensemble qui stocke uniquement des chaînes ou une carte qui stocke Map, cette approche est idéale.

Le vrai problème se pose lorsque nous voulons utiliser une liste pour stocker le type int, ou une carte pour stocker le type double comme valeur. Étant donné que les génériques ne prennent pas en charge les types de données natifs, une autre option consiste à utiliser un type wrapper à la place, nous utilisons ici List .

Cette méthode de traitement est très inutile, car un Integer est un objet complet. L'en-tête d'un objet occupe 12 octets et les propriétés int conservées à l'intérieur de celui-ci occupe un total de 16 octets. Cela consomme quatre fois plus d'espace qu'une liste de types int stockant le même nombre d'éléments ! Un problème plus grave que cela est le fait que, étant donné qu'Integer est une instance d'objet réelle, il doit être pris en compte par le ramasse-miettes pendant la phase de collecte des ordures pour être recyclé.

Pour gérer cela, nous utilisons l’impressionnante bibliothèque de collections Trove de Takipi. Trove abandonne certaines spécificités génériques au profit de collections spécialisées de types natifs plus économes en mémoire. Par exemple, nous utilisons Map, très gourmand en performances. Il existe une autre option spéciale dans Trove, qui se présente sous la forme de TIntDoubleMap

.

Carte TIntDoubleMap = new TIntDoubleHashMap();

map.put(5, 7.0);

map.put(-1, 9.999);

...

L'implémentation sous-jacente de Trove utilise des tableaux de types natifs, donc lors de l'exploitation de collections, le boxing (int->Integer) ou le déballage (Integer->int) des éléments ne se produira pas et aucun objet n'est stocké, car l'implémentation sous-jacente utilise Stockage natif de type de données.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer