BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Comprendre Les Ramasse-Miettes Classiques

Comprendre Les Ramasse-Miettes Classiques

Points Clés

  • L'hypothèse générationnelle est la clé de l'efficace des ramasse-miettes modernes
  • HotSpot compte le nombre de collections auxquelles un objet a survécu pour implémenter un ramasse-miettes générationnel
  • Le collector Parallel est toujours le ramasse-miettes Java le plus utilisé
  • La complexité algorithmique du ramasse-miettes est difficile à raisonner de manière concise
  • Les collectors qui compactent (comme ParallelOld) se comportent complètement différemment des autres collecteurs

En Java 8, le garbage collector (GC) ou ramasse-miettes par défaut pour la machine virtuelle HotSpot pour la old generation est appelé ParallelOld. Dans Java 11, celui par défaut a été remplacé par G1.

REMARQUE : Techniquement, ce remplacement s'est produit avec Java 9, mais d'importantes améliorations supplémentaires ont été apportées à G1 dans Java 10 et 11 et, dans la pratique, très peu d'entreprises utilisent autre chose que les versions LTS de Java.

Dans cet article, nous aborderons quelques notions de base de la théorie de récupération de la mémoire et comment ils sont implémentés dans les ramasse-miettes d'HotSpot. Cela ouvrira la voie à une discussion sur les raisons pour lesquelles la valeur par défaut a été modifiée et sur d'autres changements récents dans l'approche de la récupération de la mémoire en Java.

Les concepts de base

La récupération de la mémoire (garbage collection) est une activité de "ménage" du système, distincte du traitement principal de l'application, qui cherche à déterminer la mémoire qui n'est plus utilisée et à la récupérer automatiquement pour la réutiliser.

La définition de Dijkstra montre clairement que si le comptage des références est une forme de gestion automatique de la mémoire, ce n'est pas une forme de récupération de mémoire.

Le comptage des références fonctionne en mettant à jour les métadonnées par objet au cours de l'exécution du programme (par exemple, lorsque vous définissez un champ de type référence sur une nouvelle valeur). Le travail nécessaire pour mettre à jour les métadonnées a lieu sur le thread d'application et ne peut donc pas être proprement divisé en une activité distincte.

En pratique, les algorithmes de GC procèdent en partant de GC roots - un ensemble d'objets racines connus pour être vivants - et en déterminant tous les objets vivants en suivant les pointeurs.

Ces traçages implémentent des algorithmes de théorie des graphes pour partitionner la mémoire du tas en ensembles vivants et récupérables.

Dans la littérature des GC modernes, les mots concurrent et parallèle sont tous deux utilisés pour décrire les algorithmes de récupération. Ils sonnent comme s'ils devraient être des synonymes, mais en fait ils ont des significations complètement différentes :

  • Concurrent - Les threads du GC peuvent s'exécuter pendant que les threads d'application sont en cours d'exécution
  • Parallèle - Plusieurs threads sont utilisés pour exécuter l'algorithme de récupération de mémoire

Les termes ne sont pas équivalents. Au lieu de cela, ils peuvent être considérés comme la dualité de deux autres termes - concurrent est l'opposé de stop-the-world et parallèle est l'opposé de mono thread.

Les collectors de production auront un certain nombre de phases durant le cycle de l'activité du GC, et chaque phase peut présenter un mélange de caractéristiques.

Par exemple, il serait tout à fait possible d'avoir une phase qui soit monothread et concurrente, ou parallèle et stop-the-world.

REMARQUE : les collectors concurrents sont beaucoup plus complexes que les collectors stop-the-world. Non seulement cela, mais ils sont chers en termes de ressources dépensées et ont d'autres mises en garde sur leur comportement.

Quelques termes supplémentaires relatifs au GC que vous devriez connaître :

  • Exact - Un collector exact possède suffisamment d'informations sur le type pour pouvoir toujours faire la différence entre un int et un pointeur qui doit être parcouru.
  • Évacuation (Evacuating) - Déplace (évacue) tous les objets vivants vers une autre région de la mémoire. À la fin du cycle de collecte, la région de mémoire source est totalement vide et peut être réutilisée.
  • Compactage (Compacting) - À la fin du cycle de collecte, les objets survivants sont disposés en un seul bloc contigu au début de la zone de mémoire, le reste de la zone pouvant être réutilisé.

La dualité d'un schéma GC exact est un schéma conservateur. Ceux-ci manquent des informations d'un schéma exact et, par conséquent, sont généralement plus coûteux en mémoire.

Certaines sources font également référence au déplacement des collectors - qui comprend à la fois des algorithmes de compactage et d'évacuation. Cependant, les différences entre les deux sous-types sont si importantes que les regrouper n'est pas souvent utile.

Les collectors qui ne déplacent pas sont appelés in-place. Ces algorithmes nécessitent une liste libre de blocs de mémoire disponible, pour gérer la fragmentation de la mémoire et fusionner les blocs libres.

Les considérations pratiques dans HotSpot

Commençons par considérer quelques faits de base qui découlent des définitions :

  • Les objets alloués par un collector qui déplace n'ont pas d'adresses stables pendant leur durée de vie.
  • Le compactage par les collectors évitera la fragmentation de la mémoire.
  • Les collectors qui font une évacuation peuvent être écrits pour éviter la fragmentation et obtenir un compactage partiel des objets survivants.
  • Si le tas est constitué d'un seul pool de mémoire, il ne peut pas être collecté par un algorithme d'évacuation.

L'hypothèse générationnelle est un comportement d'exécution observé des systèmes orientés objet, et divise grossièrement les objets en deux catégories - les objets temporaires à durée de vie courte et les objets à durée de vie plus longue qui participent à l'ensemble de travail du programme.

REMARQUE : les collectors générationnels ne sont pas garantis pour être toujours plus efficaces que non générationnels, mais dans la pratique, presque toutes les applications bénéficient d'un GC générationnel.

La nomenclature mark-sweep-compact (par exemple selon Blackburn et McKinley) pour les algorithmes de GC est :

  • Marquage (Marking) : identification de la vivacité via un marquage dans le graphe d'objet.
  • Balayage (Sweeping) : identifier l'espace libre tout en laissant les objets vivants en place.
  • Évacuation (Evacuating) : libérer de l'espace en déplaçant les survivants dans un autre pool.
  • Compactage (Compacting) : libérer de l'espace en déplaçant les survivants dans le même pool.

Dans les GC générationnels, des algorithmes souvent complètement différents sont utilisés pour la young et la old.

Une conséquence de cela est qu'il est difficile d'étiqueter avec précision les collectors où les différentes phases utilisent des algorithmes différents. Par exemple, dans CMS, la young generation est traitée par évacuation, et la old generation est traitée par in-place mark-sweep, avec un retour à un mark-compact si le traitement concurrent échoue (par exemple en raison de la fragmentation).

Young Collections dans HotSpot

Dans HotSpot, les collectors traditionnels divisent la mémoire en 4 pools de mémoire nommés Eden, Survivor 0, Survivor 1 et Tenured. Les trois premiers d'entre eux sont collectivement regroupés dans l'espace Young et Tenured est considéré comme un espace Old.

Les espaces young sont ensuite traités lors d'une Young collection. Cela utilise une évacuation parallèle et stop-the-world pour traiter la young generation en déplaçant les objets survivants dans l'un des espaces survivor.

L'algorithme de traitement marque les données en direct dans le pool de mémoire actuellement actif, puis les évacue vers le pool inactif. À la fin du traitement, les rôles des deux espaces survivors sont inversés - le pool actif devient inactif (c'est-à-dire vide) et le pool inactif devient actif. Ceci est parfois appelé une hemispheric collection.

L'approche hémisphérique peut être une perte de mémoire. Un algorithme à un seul passage ne peut pas savoir à l'avance quelle partie de la zone de mémoire collectée est toujours active. Cela signifie que l'espace de destination (la zone vers laquelle les objets sont évacués) doit être aussi grande que la zone à nettoyer - l'algorithme a donc besoin du double de la taille réelle du survivor.

Cela signifie également que la moitié de l'espace est vide à tout moment. Ces propriétés en feraient un mauvais choix pour la old generation sur les charges de travail modernes, où l'ensemble de travail peut être assez important : en fait, aucun collector d'HotSpot pour la production n'utilise de collection hémisphérique pour la old generation.

Au lieu de cela, la collection hémisphérique est utilisée pour la young generation. Il convient bien aux charges de travail où l'hypothèse générationnelle s'applique - c'est-à-dire les zones qui sont principalement composées d'objets à récupérer. Le collector bénéficie du fait que les objets survivants peuvent être promus de la young generation vers la old.

Un autre avantage majeur des collectors par évacuation est la façon dont ils gèrent l'espace libre. L'approche la plus simple consiste à utiliser un pointeur en haut de zone pour agir comme une liste libre. À mesure que les données en direct sont évacuées, elles seront compactées "naturellement" - essentiellement gratuitement.

L'approche par évacuation, typique des collectors de young generation d'OpenJDK, utilise une passe de traçage. Cependant, les traitements sont effectués en un seul passage - pas de phases distinctes de marquage, de balayage ou compactage.

Les conséquences de l'hypothèse générationnelle

La durée de vie des objets n'est jamais connue normalement et, dans les applications réelles, elle peut changer de manière dynamique. Le suivi de l'âge réel d'un objet en temps d'horloge murale n'est donc ni réalisable ni productif.

Au lieu de cela, HotSpot enregistre le nombre de collections auxquelles un objet a survécu. Cela ne prend que quelques bits de métadonnées dans l'en-tête de l'objet, et lorsque l'objet a survécu à suffisamment de collections, il est physiquement déplacé (promu) vers la old generation et géré par un collector différent.

Ce mécanisme a une interaction intéressante avec le taux d'allocation de l'application. Si le taux d'allocation augmente, la jeune génération se remplira plus rapidement - mais la durée de vie prévue des "objets à vie courte" (mesurée en millisecondes) restera constante.

Cela peut conduire à plus d'objets survivant aux cycles de GC, ce qui à son tour peut faire déborder les espaces de survivants de la young generation avec des objets qui ne sont pas encore éligibles pour une promotion vers la old generation. Dans ce cas, la JVM n'a d'autre choix que de promouvoir certains objets à l'avance, ce qui conduit à l'effet connu sous le nom de promotion prématurée (premature promotion).

Beaucoup de ces objets sont en fait de courte durée de vie et mourront rapidement après leur arrivée dans la old generation. Malheureusement, la JVM ne dispose d'aucun mécanisme pour les récupérer si ce n'est d'attendre la prochaine collecte de la old generation.

La complexité algorithmique de la récupération de mémoire

Il est assez courant pour les développeurs de vouloir appliquer l'analyse de complexité ("notation big-O" comme on l'appelle parfois) aux algorithmes de récupération de mémoire. Cependant, dans la pratique, cette approche n'est pas réellement très satisfaisante.

Naïvement, on pourrait supposer que la complexité temporelle des phases de marquage et de compactage serait linéaire selon la taille de l'ensemble des objets vivants, tandis que la phase de balayage serait linéaire dans la taille de la taille globale du tas.

Cependant, même en laissant de côté le problème selon lequel la séparation des phases peut ne pas être propre dans les implémentations réelles (comme pour les collectors générationnels d'HotSpot discutés ci-dessus), un problème plus profond existe.

C'est le simple fait que les ramasse-miettes sont, par leur nature même, parmi les algorithmes les plus généraux utilisés. Cela signifie que l'hypothèse inhérente à l'analyse big-O - que ce qui importe, c'est le comportement de limitation à mesure que la taille de l'ensemble de données augmente - n'est tout simplement pas valide.

Les algorithmes de GC pour les systèmes de production doivent se comporter de manière acceptable sur toute une gamme d'entrées et de charges de travail possibles. Leur comportement asymptotique n'est pas du tout un proxy approprié pour leur performance globale.

Autrement dit, la taille de l'ensemble d'objets vivants et du segment de mémoire peut varier essentiellement indépendamment (par exemple en raison de topologies de graphe d'objets différentes). Cela signifie que les facteurs d'échelle multiplicatifs peuvent avoir des effets très différents pour différentes charges de travail.

Par exemple, le compactage nécessite la copie d'octets, donc bien qu'une phase de compactage soit linéaire dans la taille de l'ensemble d'objets vivants, l'un des autres facteurs serait la taille des objets à déplacer. Dans le cas d'un grand nombre de tableaux avec de nombreux éléments, cela pourrait rapidement submerger toutes les autres considérations pour le temps de pause.

Il existe également des effets de second ordre bien connus pour les différentes formes d'algorithmes de GC. Par exemple, lorsque vous effectuez un compactage sur une zone de mémoire qui a très peu d'objets survivants ("tas clairsemé"), les données vivantes seront fusionnées en une zone beaucoup plus dense. Cette zone sera alors beaucoup moins clairsemée pour les cycles GC suivants, à condition que les objets aient une durée de vie vraiment longue.

En comparant cela à un collector in-place, comme CMS, nous pouvons voir que les objets à longue durée de vie resteront peu répartis pendant toute la durée de vie du programme. En effet, au fil du temps, l'espace libre deviendra de plus en plus fragmenté et la gestion de la liste des blocs mémoire libres (la liste libre) deviendra de plus en plus coûteuse.

Dans l'ensemble, les modèles de coûts d'espaces et temporels pour les différentes approches de GC sont très différents et la complexité algorithmique naïve n'est tout simplement pas très utile. Dans le cas de HotSpot, le mode de défaillance ultime d'un collector in-place consiste à recourir à un collector de compactage si un espace contigu suffisant ne peut pas être trouvé. Il s'agit d'un effet comportemental important et non couvert par une approche simpliste.

Résumé

Nous avons discuté de certains des aspects fondamentaux de la récupération de mémoire telle qu'elle est implémentée dans la machine virtuelle Java. La collecte des objets inutilisés est un domaine mature de l'informatique, et les collectors présents dans HotSpot sont robustes et bien testés et devraient bien se comporter sur une très grande variété de charges de travail. La plupart des applications Java n'ont tout simplement pas à se soucier trop du comportement du GC.

Pour le petit pourcentage de cas qui sont sensibles au comportement du GC, une compréhension plus approfondie des principes de la récupération de mémoire (et de la façon dont il est implémenté dans la JVM) est un outil très utile pour le développeur dans sa boîte à outils.

Dans les versions récentes de Java, le sous-système de récupération de mémoire est redevenu un domaine actif d'améliorations. Cependant, pour bien comprendre ces changements, il est nécessaire d'avoir une bonne compréhension des bases. Un prochain article traitera de ces mises à jour en détail et expliquera, par exemple, pourquoi le collector par défaut a été modifié et ce que cela signifie pour les équipes qui mettent à niveau vers Java 11.

A propos de l'auteur

Ben Evans est co-fondateur de jClarity, une société d'optimisation des performances de JVM. Il est un des organisateurs du LJC (London's JUG) et membre du comité exécutif du JCP, aidant à définir des normes pour l'écosystème Java. Ben est un champion Java; 3 fois JavaOne Rockstar; auteur de "The Well-Grounded Java Developer", la nouvelle édition de "Java in a Nutshell" et "Optimizing Java" Il est un conférencier régulier sur la plate-forme Java, les performances, l'architecture, la concurrence, les startups et les sujets connexes. Ben est parfois disponible pour parler, enseigner, écrire et de la consultance - veuillez le contacter pour plus de détails.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT