Le mécanisme de garbage collection de la plate-forme Java a considérablement amélioré l'efficacité des développeurs, mais un garbage collector mal implémenté peut consommer trop de ressources d'application. Dans la troisième partie de la série d'optimisation des performances des machines virtuelles Java, Eva Andreasson présente aux débutants Java le modèle de mémoire et le mécanisme de récupération de place de la plate-forme Java. Elle explique pourquoi la fragmentation (et non le garbage collection) est le principal problème de performances des applications Java, et pourquoi le garbage collection et le compactage générationnels sont actuellement les principaux moyens (mais pas les plus innovants) de gérer la fragmentation des applications Java.
Le but du garbage collection (GC) est de libérer la mémoire occupée par les objets Java qui ne sont plus référencés par aucun objet actif. Il s'agit de la partie centrale du mécanisme de gestion dynamique de la mémoire de la machine virtuelle Java. Au cours d'un cycle typique de garbage collection, tous les objets qui sont encore référencés (et donc accessibles) sont conservés, tandis que ceux qui ne sont plus référencés sont libérés et l'espace qu'ils occupent est récupéré pour être alloué à de nouveaux objets.
Afin de comprendre le mécanisme de récupération de place et les différents algorithmes de récupération de place, vous devez d'abord connaître le modèle de mémoire de la plate-forme Java.
Garbage collection et modèle de mémoire de la plateforme Java
Lorsque vous démarrez un programme Java à partir de la ligne de commande et spécifiez le paramètre de démarrage -Xmx (par exemple : java -Xmx:2g MyApp), la mémoire de la taille spécifiée est allouée au processus Java, appelé tas Java. . Cet espace d'adressage mémoire dédié est utilisé pour stocker les objets créés par les programmes Java (et parfois la JVM). Au fur et à mesure que l'application s'exécute et alloue continuellement de la mémoire pour de nouveaux objets, le tas Java (c'est-à-dire l'espace d'adressage mémoire dédié) se remplira lentement.
Finalement, le tas Java se remplira, ce qui signifie que le thread d'allocation de mémoire ne peut pas trouver un espace contigu suffisamment grand pour allouer de la mémoire au nouvel objet. À ce moment-là, la JVM décide d'avertir le garbage collector et de démarrer le garbage collection. Le garbage collection peut également être déclenché en appelant System.gc() dans le programme, mais l'utilisation de System.gc() ne garantit pas que le garbage collection sera effectué. Avant tout garbage collection, le mécanisme de garbage collection déterminera d'abord s'il est sûr d'effectuer un garbage collection. Lorsque tous les threads actifs de l'application sont à un point sûr, un garbage collection peut être démarré. Par exemple, le garbage collection ne peut pas être effectué lorsque la mémoire est allouée à un objet, ou le garbage collection ne peut pas être effectué pendant l'optimisation des instructions du processeur, car le contexte risque d'être perdu et le résultat final sera incorrect.
Le garbage collector ne peut récupérer aucun objet avec des références actives, ce qui violerait la spécification de la machine virtuelle Java. Il n'est pas nécessaire de recycler les objets morts immédiatement, car les objets morts seront éventuellement recyclés lors du ramassage des ordures ultérieur. Bien qu'il existe de nombreuses façons d'implémenter le garbage collection, les deux points ci-dessus sont les mêmes pour toutes les implémentations du garbage collection. Le véritable défi du garbage collection est de savoir comment identifier si un objet est vivant et comment récupérer de la mémoire sans affecter autant que possible l'application. Par conséquent, le garbage collector a les deux objectifs suivants :
1. Libérez rapidement la mémoire non référencée pour répondre aux besoins d'allocation de mémoire de l'application afin d'éviter un débordement de mémoire.
2. Minimisez l'impact sur les performances des applications en cours d'exécution (latence et débit) lors de la récupération de mémoire.
Deux types de collecte des déchets
Dans le premier article de cette série, j'ai présenté deux méthodes de garbage collection, à savoir le comptage de références et la collecte de suivi. Nous explorons ensuite ces deux approches plus en détail et introduisons quelques algorithmes de collecte de traces utilisés dans les environnements de production.
Collecteur de comptage de référence
Le collecteur de comptage de références enregistre le nombre de références pointant vers chaque objet Java. Une fois que le nombre de références pointant vers un objet atteint 0, l'objet peut être recyclé immédiatement. Cette immédiateté est le principal avantage d'un collecteur à comptage de références, et il n'y a pratiquement aucune surcharge liée à la maintenance d'une mémoire vers laquelle aucune référence ne pointe, mais garder la trace du dernier décompte de références pour chaque objet coûte cher.
La principale difficulté du collecteur de comptage de références est de savoir comment garantir l'exactitude du comptage de références. Une autre difficulté bien connue est de savoir comment gérer les références circulaires. Si deux objets se référencent l'un l'autre et ne sont pas référencés par d'autres objets actifs, alors la mémoire des deux objets ne sera jamais récupérée car le nombre de références pointant vers aucun objet est 0. Le recyclage de la mémoire des structures de référence circulaires nécessite une analyse majeure (Note du traducteur : analyse globale sur le tas Java), ce qui augmentera la complexité de l'algorithme et entraînera ainsi une surcharge supplémentaire pour l'application.
collecteur de traces
Le collecteur de traçage est basé sur l'hypothèse que tous les objets vivants peuvent être trouvés en itérant des références (références et références de références) à un ensemble initial connu d'objets vivants. L'ensemble initial d'objets actifs (également appelés objets racine) peut être déterminé en analysant les registres, les objets globaux et les cadres de pile. Après avoir déterminé l'ensemble initial d'objets, le collecteur de suivi suit les relations de référence de ces objets et marque les objets pointés par les références comme objets actifs en séquence, de sorte que l'ensemble d'objets actifs connus continue de s'étendre. Ce processus se poursuit jusqu'à ce que tous les objets référencés soient marqués comme objets actifs et que la mémoire des objets qui n'ont pas été marqués soit récupérée.
Le collecteur de suivi diffère du collecteur de comptage de références principalement en ce qu'il peut gérer des structures de référence circulaires. La plupart des collectionneurs de traçage découvrent des objets non référencés dans des structures de référence circulaires pendant la phase de marquage.
Le collecteur de traçage est la méthode de gestion de mémoire la plus couramment utilisée dans les langages dynamiques et est actuellement la méthode la plus courante en Java. Elle est également vérifiée dans les environnements de production depuis de nombreuses années. Ci-dessous, je présenterai le collecteur de traces en commençant par quelques algorithmes pour implémenter la collecte de traces.
Algorithme de collecte de traces
Les garbage collectors de copie et les garbage collectors par balayage de marquage ne sont pas nouveaux, mais ils restent les deux algorithmes les plus courants pour implémenter les collectes de suivi aujourd'hui.
Copier le garbage collector
Le garbage collector de copie traditionnel utilise deux espaces d'adressage dans le tas (c'est-à-dire depuis l'espace et vers l'espace). Lorsque le garbage collection est effectué, les objets actifs dans l'espace depuis sont copiés vers l'espace vers lorsque tous les objets actifs dans l'espace depuis. sont supprimés (Note du traducteur : après avoir copié vers l'espace ou l'ancienne génération), l'intégralité de l'espace peut être recyclée. Lorsque l'espace est à nouveau alloué, l'espace vers sera utilisé en premier (Note du traducteur : c'est-à-dire l'espace vers). du tour précédent sera utilisé comme nouveau tour de l’espace depuis l’espace).
Au début de la mise en œuvre de cet algorithme, l'espace depuis l'espace et l'espace vers ont continuellement changé de position, c'est-à-dire que lorsque l'espace vers est plein et que le garbage collection est déclenché, l'espace vers devient l'espace depuis, comme le montre la figure 1. .
Figure 1 Séquence traditionnelle de récupération de place pour les copies
Le dernier algorithme de copie permet d'utiliser n'importe quel espace d'adressage du tas tant vers l'espace que depuis l'espace. De cette façon, ils n’ont pas besoin d’échanger leurs positions, mais simplement de changer logiquement de position.
L'avantage du collecteur de copie est que les objets copiés dans l'espace sont disposés de manière compacte et qu'il n'y a aucune fragmentation. La fragmentation est un problème courant auquel sont confrontés les autres éboueurs, et c'est aussi le problème principal dont je parlerai plus tard.
Inconvénients du collecteur de copies
D'une manière générale, le collecteur de copie est stop-the-world, ce qui signifie que tant que le garbage collection est en cours, l'application ne peut pas s'exécuter. Avec cette implémentation, plus vous devez copier d’éléments, plus l’impact sur les performances des applications est important. Ceci constitue un inconvénient pour les applications sensibles au temps de réponse. Lorsque vous utilisez le collecteur de copies, vous devez également envisager le pire scénario (c'est-à-dire que tous les objets dans l'espace depuis sont des objets actifs. À ce stade, vous devez préparer un espace suffisamment grand pour déplacer ces objets actifs, de sorte qu'ils soient actifs). L'espace doit être suffisamment grand pour installer tous les objets dans l'espace. En raison de cette limitation, l'utilisation de la mémoire par l'algorithme de copie est légèrement insuffisante (Note du traducteur : dans le pire des cas, l'espace vers doit être de la même taille que l'espace depuis, donc seulement 50 % d'utilisation).
collecteur de marquage
La plupart des JVM commerciales déployées dans des environnements de production d'entreprise utilisent un collecteur de marquage (ou marquage), car il ne reproduit pas l'impact d'un ramasse-miettes sur les performances des applications. Les collectionneurs de marques les plus connus incluent CMS, G1, GenPar et DeterministicGC.
Le collecteur de marquage et de balayage suit les références d'objets et marque chaque objet trouvé comme étant actif à l'aide d'un bit d'indicateur. Cet indicateur correspond généralement à une adresse ou à un groupe d'adresses sur le tas. Par exemple : le bit actif peut être un bit dans l'en-tête de l'objet (Note du traducteur : bit) ou un vecteur bit ou un bitmap.
Une fois le marquage terminé, la phase de nettoyage est entrée. La phase de nettoyage parcourt généralement à nouveau le tas (pas seulement les objets marqués comme actifs, mais l'ensemble du tas) pour localiser les espaces d'adressage de mémoire contigus non marqués (la mémoire non marquée est libre et recyclable), puis le collecteur les organise en listes libres. Le garbage collector peut avoir plusieurs listes libres (généralement divisées en fonction de la taille du bloc de mémoire). Certains collecteurs JVM (par exemple : JRockit Real Time) divisent même dynamiquement la liste libre en fonction de l'analyse des performances de l'application et des statistiques sur la taille des objets.
Après la phase de nettoyage, l'application peut à nouveau allouer de la mémoire. Lors de l'allocation de mémoire pour un nouvel objet de la liste libre, le bloc de mémoire nouvellement alloué doit s'adapter à la taille du nouvel objet, ou à la taille moyenne de l'objet du thread, ou à la taille TLAB de l'application. La recherche de blocs de mémoire de taille appropriée pour les nouveaux objets permet d'optimiser la mémoire et de réduire la fragmentation.
Mark - Effacer les défauts du collectionneur
Le temps d'exécution de la phase de marquage dépend du nombre d'objets actifs dans le tas, tandis que le temps d'exécution de la phase de nettoyage dépend de la taille du tas. Par conséquent, pour les situations où le paramètre de tas est grand et où il y a de nombreux objets actifs dans le tas, l'algorithme de balayage de marquage aura un certain temps de pause.
Pour les applications gourmandes en mémoire, vous pouvez ajuster les paramètres de garbage collection en fonction de différents scénarios et besoins d'application. Dans de nombreux cas, cet ajustement reporte au moins le risque posé par la phase de marquage/balayage au SLA de l'application ou du contrat de service (SLA fait ici référence au temps de réponse que l'application doit atteindre). Mais le réglage n'est efficace que pour des charges spécifiques et des taux d'allocation de mémoire. Les changements de charge ou les modifications apportées à l'application elle-même nécessitent un nouveau réglage.
Implémentation d'un collecteur de balayage de marques
Il existe au moins deux méthodes commercialement éprouvées pour mettre en œuvre la collecte des déchets par balayage. L’un est le garbage collection parallèle et l’autre est le garbage collection simultané (ou la plupart du temps simultané).
Collecteur parallèle
La collecte parallèle signifie que les ressources sont utilisées en parallèle par les threads de garbage collection. La plupart des implémentations commerciales de collecte parallèle sont des collecteurs stop-the-world, dans lesquels tous les threads d'application sont mis en pause jusqu'à ce qu'un garbage collection soit terminé. Étant donné que les garbage collector peuvent utiliser les ressources de manière efficace, ils fonctionnent généralement mieux dans les tests de débit, comme. SPECjbb. Si le débit est essentiel pour votre application, un garbage collector parallèle est un bon choix.
Le principal coût de la collecte parallèle (en particulier pour les environnements de production) est que les threads d'application ne peuvent pas fonctionner correctement pendant la récupération de place, tout comme le collecteur de copie. Par conséquent, l’utilisation de collecteurs parallèles aura un impact significatif sur les applications sensibles au temps de réponse. En particulier lorsqu'il existe de nombreuses structures d'objets actifs complexes dans l'espace du tas, de nombreuses références d'objets doivent être suivies. (N'oubliez pas que le temps nécessaire au collecteur de marquage par balayage pour récupérer la mémoire dépend du temps nécessaire pour suivre la collection d'objets actifs plus le temps nécessaire pour parcourir l'ensemble du tas.) Avec l'approche parallèle, l'application est mise en pause pendant le toute la durée de la collecte des déchets.
collecteur simultané
Les garbage collector simultanés sont plus adaptés aux applications sensibles au temps de réponse. La concurrence signifie que le thread de garbage collection et le thread d’application s’exécutent simultanément. Le thread de garbage collection ne possède pas toutes les ressources, il doit donc décider quand démarrer un garbage collection, en laissant suffisamment de temps pour suivre la collecte d'objets active et récupérer la mémoire avant que la mémoire de l'application ne déborde. Si le garbage collection n'est pas terminé à temps, l'application générera une erreur de dépassement de mémoire. D'un autre côté, vous ne voulez pas que le garbage collection prenne trop de temps car cela consommerait les ressources de l'application et affecterait le débit. Maintenir cet équilibre nécessite des compétences, c'est pourquoi des heuristiques sont utilisées pour déterminer quand démarrer le garbage collection et quand choisir les optimisations du garbage collection.
Une autre difficulté consiste à déterminer quand il est sûr d'effectuer certaines opérations (opérations qui nécessitent un instantané de tas complet et précis), comme par exemple avoir besoin de savoir quand la phase de marquage est terminée pour pouvoir passer à la phase de nettoyage. Ce n'est pas un problème pour un collecteur parallèle stop-the-world, car le monde est déjà en pause (Note du traducteur : le thread d'application est en pause et le thread de garbage collection monopolise les ressources). Mais pour les collecteurs simultanés, il peut ne pas être sûr de passer immédiatement de la phase de marquage à la phase de nettoyage. Si un thread d'application modifie une partie de la mémoire qui a été suivie et marquée par le garbage collector, de nouvelles références non marquées peuvent être générées. Dans certaines implémentations de collections simultanées, cela peut entraîner le blocage de l'application dans une boucle d'annotations répétées pendant une longue période sans pouvoir obtenir de la mémoire libre lorsque l'application en a besoin.
D'après la discussion jusqu'à présent, nous savons qu'il existe de nombreux garbage collector et algorithmes de garbage collection, chacun adapté à des types d'applications spécifiques et à des charges différentes. Pas seulement des algorithmes différents, mais des implémentations d’algorithmes différentes. Par conséquent, il est préférable de comprendre les besoins de l’application et ses propres caractéristiques avant de spécifier un garbage collector. Ensuite, nous présenterons quelques pièges du modèle de mémoire de la plate-forme Java. Les pièges ici font référence à certaines hypothèses que les programmeurs Java sont enclins à faire dans un environnement de production en évolution dynamique et qui aggravent les performances des applications.
Pourquoi le réglage ne peut pas remplacer le garbage collection
La plupart des programmeurs Java savent qu'il existe de nombreuses options pour optimiser les programmes Java. Plusieurs paramètres facultatifs de JVM, de garbage collector et de réglage des performances permettent aux développeurs de consacrer beaucoup de temps à un réglage sans fin des performances. Cela a conduit certaines personnes à conclure que le garbage collection est mauvais et que le réglage pour que les garbage collection se produisent moins fréquemment ou durent plus longtemps est une bonne solution de contournement, mais cela est risqué.
Envisagez de régler pour une application spécifique. La plupart des paramètres de réglage (tels que le taux d'allocation de mémoire, la taille de l'objet, le temps de réponse) sont basés sur le taux d'allocation de mémoire de l'application (Note du traducteur : ou d'autres paramètres) en fonction du volume de données de test actuel. Cela peut finalement conduire aux deux résultats suivants :
1. Un cas d’utilisation qui réussit les tests échoue en production.
2. Les modifications du volume de données ou les modifications des applications nécessitent un réajustement.
Le réglage est itératif, et les garbage collectors simultanés en particulier peuvent nécessiter beaucoup de réglages (en particulier dans un environnement de production). Des heuristiques sont nécessaires pour répondre aux besoins de l’application. Afin de répondre au pire des cas, le résultat du réglage peut être une configuration très rigide, ce qui entraîne également un gaspillage important de ressources. Cette approche de réglage est une quête chimérique. En fait, plus vous optimisez le garbage collector pour qu'il corresponde à une charge spécifique, plus vous vous éloignez de la nature dynamique du runtime Java. Après tout, combien d’applications ont une charge stable et quelle est la fiabilité de cette charge ?
Alors, si vous ne vous concentrez pas sur le réglage, que pouvez-vous faire pour éviter les erreurs de mémoire insuffisante et améliorer les temps de réponse ? La première chose à faire est de trouver les principaux facteurs affectant les performances des applications Java.
fragmentation
Le facteur qui affecte les performances des applications Java n'est pas le garbage collector, mais la fragmentation et la manière dont le garbage collector gère la fragmentation. Ce qu'on appelle la fragmentation est un état dans lequel il y a de l'espace libre dans l'espace du tas, mais il n'y a pas d'espace mémoire contigu suffisamment grand pour allouer de la mémoire à de nouveaux objets. Comme mentionné dans le premier article, la fragmentation de la mémoire est soit un TLAB de l'espace restant dans le tas, soit l'espace occupé par de petits objets libérés parmi les objets à longue durée de vie.
Au fil du temps et à mesure que l'application s'exécute, cette fragmentation se propage dans tout le tas. Dans certains cas, l’utilisation de paramètres optimisés de manière statique peut s’avérer pire car ils ne parviennent pas à répondre aux besoins dynamiques de l’application. Les applications ne peuvent pas utiliser efficacement cet espace fragmenté. Ne rien faire entraînera des garbage collection successifs où le garbage collector tentera de libérer de la mémoire pour l'allouer à de nouveaux objets. Dans le pire des cas, même les garbage collection successifs ne peuvent pas libérer plus de mémoire (trop de fragmentation), et la JVM doit alors générer une erreur de dépassement de mémoire. Vous pouvez résoudre la fragmentation en redémarrant l'application afin que le tas Java dispose d'un espace mémoire contigu pour allouer de nouveaux objets. Le redémarrage du programme entraîne un temps d'arrêt et, après un certain temps, le tas Java se remplit à nouveau de fragments, forçant un autre redémarrage.
Les erreurs de mémoire insuffisante qui bloquent le processus et les journaux indiquant que le garbage collector est surchargé indiquent que le garbage collection tente de libérer de la mémoire et que le tas est fortement fragmenté. Certains programmeurs tenteront de résoudre le problème de fragmentation en optimisant à nouveau le garbage collector. Mais je pense que nous devrions trouver des moyens plus innovants pour résoudre ce problème. Les sections suivantes se concentreront sur deux solutions à la fragmentation : le garbage collection générationnel et le compactage.
Collecte des déchets générationnelle
Vous avez peut-être entendu la théorie selon laquelle la plupart des objets dans un environnement de production ont une durée de vie de courte durée. Le garbage collection générationnel est une stratégie de garbage collection dérivée de cette théorie. Dans le garbage collection générationnel, nous divisons le tas en différents espaces (ou générations), et chaque espace stocke des objets d'âges différents. L'âge d'un objet est le nombre de cycles de garbage collection auxquels l'objet a survécu (c'est-à-dire). quel âge a l'objet). toujours référencé après les cycles de garbage collection).
Lorsqu'il n'y a plus d'espace à allouer dans la nouvelle génération, les objets actifs de la nouvelle génération seront déplacés vers l'ancienne génération (il n'y a généralement que deux générations. Note du traducteur : seuls les objets qui répondent à un certain âge seront déplacés vers la nouvelle génération. génération).Le garbage collection de générations utilise souvent un collecteur de copie unidirectionnel. Certaines JVM plus modernes utilisent des collecteurs parallèles dans la nouvelle génération. Bien entendu, différents algorithmes de garbage collection peuvent être implémentés pour la nouvelle génération et l'ancienne génération. Si vous utilisez un collectionneur parallèle ou un collectionneur à copie, votre jeune collectionneur est un collectionneur stop-the-world (voir explication précédente).
L'ancienne génération est attribuée aux objets qui ont été déplacés hors de la nouvelle génération. Ces objets ont été référencés depuis longtemps ou sont référencés par une collection d'objets de la nouvelle génération. Parfois, les gros objets sont attribués directement à l’ancienne génération car le coût de déplacement des gros objets est relativement élevé.
Technologie de collecte des déchets générationnelle
Dans le ramassage des ordures générationnel, le ramassage des ordures s'effectue moins fréquemment dans l'ancienne génération et plus fréquemment dans la nouvelle génération, et nous espérons également que le cycle de ramassage des ordures dans la nouvelle génération sera plus court. Dans de rares cas, la jeune génération peut être collectée plus fréquemment que l'ancienne génération. Cela peut se produire si vous rendez la jeune génération trop grande et que la plupart des objets de votre application durent longtemps. Dans ce cas, si l’ancienne génération est trop petite pour accueillir tous les objets à longue durée de vie, le garbage collection de l’ancienne génération aura également du mal à libérer de l’espace pour les objets déplacés. Cependant, d’une manière générale, le garbage collection générationnel peut permettre aux applications d’obtenir de meilleures performances.
Un autre avantage de la division de la nouvelle génération est qu’elle résout dans une certaine mesure le problème de la fragmentation ou retarde le pire des cas. Ces petits objets avec une courte durée de survie peuvent avoir causé des problèmes de fragmentation, mais ils sont tous nettoyés dans le garbage collection de nouvelle génération. Étant donné que les objets à longue durée de vie se voient attribuer un espace plus compact lorsqu'ils sont déplacés vers l'ancienne génération, l'ancienne génération est également plus compacte. Au fil du temps (si votre application s'exécute suffisamment longtemps), l'ancienne génération deviendra également fragmentée, nécessitant l'exécution d'un ou plusieurs garbage collection complets, et la JVM peut également générer des erreurs de mémoire insuffisante. Mais créer une nouvelle génération repousse le pire des cas, ce qui est suffisant pour de nombreuses applications. Pour la plupart des applications, cela réduit la fréquence du garbage collection et le risque d'erreurs de mémoire insuffisante.
Optimiser le garbage collection générationnel
Comme mentionné précédemment, l'utilisation du garbage collection générationnel entraîne un travail de réglage répété, tel que l'ajustement de la taille de la jeune génération, du taux de promotion, etc. Je ne peux pas insister sur le compromis pour l'exécution d'une application spécifique : le choix d'une taille fixe optimise l'application, mais cela réduit également la capacité du ramasse-miettes à faire face aux changements dynamiques, qui sont inévitables.
Le premier principe pour la nouvelle génération est de l'augmenter autant que possible tout en garantissant le délai lors du garbage collection stop-the-world, et en même temps, de réserver suffisamment d'espace dans le tas pour les objets survivant à long terme. Voici quelques facteurs supplémentaires à prendre en compte lors du réglage d’un ramasse-miettes générationnel :
1. La plupart des nouvelles générations sont des éboueurs qui stoppent le monde. Plus le paramètre de nouvelle génération est grand, plus le temps de pause correspondant est long. Par conséquent, pour les applications qui sont fortement affectées par les temps de pause du garbage collection, réfléchissez attentivement à la taille de la jeune génération.
2. Différents algorithmes de garbage collection peuvent être utilisés sur différentes générations. Par exemple, le garbage collection parallèle est utilisé dans la jeune génération et le garbage collection simultané est utilisé dans l'ancienne génération.
3. Lorsqu'on constate que des promotions fréquentes (Note du traducteur : passer de la nouvelle génération à l'ancienne génération) échouent, cela signifie qu'il y a trop de fragments dans l'ancienne génération, ce qui signifie qu'il n'y a pas assez d'espace dans l'ancienne génération. pour stocker les objets déplacés de la nouvelle génération. À ce stade, vous pouvez ajuster le taux de promotion (c'est-à-dire ajuster l'âge de la promotion), ou vous assurer que l'algorithme de récupération de place de l'ancienne génération effectue la compression (discuté dans le paragraphe suivant) et ajuster la compression en fonction de la charge de l'application. . Il est également possible d'augmenter la taille du tas et de chaque génération, mais cela prolongera encore le temps de pause sur l'ancienne génération. Sachez que la fragmentation est inévitable.
4. La collecte des déchets générationnelle est la plus adaptée à de telles applications. Ils contiennent de nombreux petits objets avec des temps de survie courts. De nombreux objets sont recyclés lors du premier cycle de collecte des déchets. Pour de telles applications, le garbage collection générationnel peut réduire efficacement la fragmentation et retarder l’impact de la fragmentation.
compression
Bien que le garbage collection générationnel retarde l’apparition d’erreurs de fragmentation et de manque de mémoire, la compression est la seule véritable solution au problème de fragmentation. Le compactage est une stratégie de garbage collection qui libère des blocs de mémoire contigus en déplaçant les objets, libérant ainsi suffisamment d'espace pour créer de nouveaux objets.
Le déplacement d'objets et la mise à jour des références d'objets sont des opérations d'arrêt du monde qui entraîneront une certaine consommation (à une exception près, qui sera abordée dans le prochain article de cette série). Plus il y a d’objets qui survivent, plus le temps de pause provoqué par le compactage est long. Dans les situations où il reste peu d'espace restant et une fragmentation sévère (généralement parce que le programme est exécuté depuis longtemps), il peut y avoir une pause de quelques secondes dans les zones de compactage contenant de nombreux objets actifs, et à l'approche d'un débordement de mémoire, la compression du le tas entier peut même prendre des dizaines de secondes.
Le temps de pause pour le compactage dépend de la quantité de mémoire à déplacer et du nombre de références à mettre à jour. L'analyse statistique montre que plus le tas est grand, plus le nombre d'objets vivants qui doivent être déplacés et les références mises à jour est important. Le temps de pause est d'environ 1 seconde pour chaque 1 Go à 2 Go d'objets actifs déplacés, et pour un tas de 4 Go, il est probable qu'il y ait 25 % d'objets actifs, il y aura donc des pauses occasionnelles d'environ 1 seconde.
Mur de mémoire de compression et d’application
Le mur de mémoire d'application fait référence à la taille du tas qui peut être définie avant une pause provoquée par le garbage collection (par exemple : compactage). Selon le système et l'application, la plupart des murs de mémoire des applications Java vont de 4 Go à 20 Go. C'est pourquoi la plupart des applications d'entreprise sont déployées sur plusieurs JVM plus petites plutôt que sur quelques JVM plus grandes. Considérons ceci : combien de conceptions et de déploiements d'applications Java d'entreprise modernes sont définis par les limitations de compression de la JVM. Dans ce cas, afin de contourner le temps de pause de la défragmentation du tas, nous avons opté pour un déploiement multi-instance plus coûteux à gérer. C'est un peu étrange compte tenu des grandes capacités de stockage du matériel actuel et du besoin de mémoire accrue pour les applications Java d'entreprise. Pourquoi seuls quelques Go de mémoire sont définis pour chaque instance. La compression simultanée brisera le mur de mémoire, ce qui fera l'objet de mon prochain article.
Résumer
Cet article est un article d'introduction sur le garbage collection pour vous aider à comprendre les concepts et les mécanismes du garbage collection et, espérons-le, vous motiver à lire d'autres articles connexes. La plupart des éléments abordés ici existent depuis longtemps et de nouveaux concepts seront introduits dans le prochain article. Par exemple, la compression simultanée est actuellement implémentée par la JVM Zing d'Azul. Il s'agit d'une technologie émergente de garbage collection qui tente même de redéfinir le modèle de mémoire Java, d'autant plus que la mémoire et la puissance de traitement continuent de s'améliorer aujourd'hui.
Voici quelques points clés sur le garbage collection que j’ai résumés :
1. Différents algorithmes et implémentations de garbage collection s'adaptent aux différents besoins des applications. Le garbage collector de suivi est le garbage collector le plus couramment utilisé dans les machines virtuelles Java commerciales.
2. Le garbage collection parallèle utilise toutes les ressources en parallèle lors de l'exécution du garbage collection. Il s'agit généralement d'un garbage collector stop-the-world et a donc un débit plus élevé, mais les threads de travail de l'application doivent attendre la fin du thread de garbage collection, ce qui a un certain impact sur le temps de réponse de l'application.
3. Collecte de place simultanée : pendant que la collecte est en cours d'exécution, le thread de travail de l'application est toujours en cours d'exécution. Un garbage collector simultané doit terminer le garbage collection avant que l’application n’ait besoin de la mémoire.
4. Le garbage collection générationnel aide à retarder la fragmentation, mais il ne peut pas l’éliminer. Le garbage collection générationnel divise le tas en deux espaces, un espace pour les nouveaux objets et l'autre pour les anciens objets. Le garbage collection générationnel convient aux applications comportant de nombreux petits objets ayant une durée de vie courte.
5. La compression est le seul moyen de résoudre la fragmentation. La plupart des garbage collector effectuent la compression de manière stop-the-world. Plus le programme s'exécute longtemps, plus les références d'objet sont complexes et plus les tailles des objets sont inégalement réparties, ce qui entraîne des temps de compression plus longs. La taille du tas affecte également le temps de compactage, car il peut y avoir davantage d'objets actifs et de références qui doivent être mis à jour.
6. Le réglage permet de retarder les erreurs de dépassement de mémoire. Mais le résultat d’un réglage excessif est une configuration rigide. Avant de commencer le réglage via une approche par essais et erreurs, assurez-vous de bien comprendre la charge de votre environnement de production, les types d'objet de votre application et les caractéristiques de vos références d'objet. Les configurations trop rigides peuvent ne pas être en mesure de gérer les charges dynamiques, alors assurez-vous de comprendre les conséquences lors de la définition de valeurs non dynamiques.
Le prochain article de cette série est : Une discussion approfondie de l’algorithme de récupération de place C4 (Concurrent Continuously Compacting Collector), alors restez à l’écoute !
(Fin du texte intégral)