Le « garbage collector » : automatique ou pas ?

Le « garbage collector » a pour fonction de « nettoyer » les espaces mémoires dédiés aux objets, instances de classes. De manière à les réattribuer…pour le plus grand bien d’applications qui disposent ainsi d’espaces d’exécution plus importants.

Le concept de Garbage Collector » (ramasse-miettes) a plus de cinquante ans d’existence (John McCarthy et langage Lisp). Il fait partie de l’environnement des langages objet, de Java entre-autres et suscite actuellement un intérêt accru de la part des éditeurs, qui y voient un moyen d’améliorer fortement les performances de leurs langages et frameworks. C’est ce qui s’est passé avec les dernières versions 11 et 12 du JDK Java, dans lesquelles G1 (Garbage First) a été fortement modifié. D’où notre coup de projecteur, sur une technologie, certes ancienne, mais mal connue et qui est appelée à se transformer grâce à l’apport de l’Intelligence Artificielle.

 

Le principe du « ramasse-miettes »

Le ramasse-miettes est intimement lié au fonctionnement des langages objet (mais pas seulement) et au processus d’instanciation de classes, qui produit des objets dotés de propriétés et de comportements, stockés « quelque part », le temps où ils sont utiles, le « quelque part » étant la mémoire « heap » dans une architecture Java.

L’un des problèmes que posent les objets est de les « tuer » une fois qu’ils sont censés ne plus servir, de manière à récupérer la mémoire qui leur a été attribuée.

Il y a ici deux manières de fonctionner. Soit en automatique et c’est un composant du système, le « garbage collector », qui fait le travail, sans qu’il y ait quoi que ce soit à programmer, soit il faut coder le « nettoyage » en supprimant les objets dès lors que l’on considère qu’ils n’ont plus d’utilité. On dit qu’ils ne sont plus référençables.

Deux éléments très importants dans l’architecture de la JVM Java HotSpott : la « heap memory » où sont stockées les instances de classes et le « garbage collector » qui fait la « chasse » aux objets inutiles, car non référencés.

 

La polémique

Depuis des années les développeurs entretiennent une polémique sur le fait de savoir si le « garbage collector » présente un intérêt et s’il ne vaut pas mieux tout programmer soi-même, sachant qu’on le fera toujours mieux qu’un automate (...).

Les avantages d’un GC sont évidents :

  • ·         on n’a pas à se préoccuper de la durée de vie d’un objet
  • ·         on peut partager un objet avec d’autres objets et exploiter le pattern de classe immuable, pour éviter d’instancier des objets inutiles, le GC se chargeant de tout gérer
  • ·         quand il est bien conçu, le GC optimise les opérations d’allocations/désallocations grâce à des traitements regroupés, susceptibles de porter sur des milliers d’objets en une seule passe. Plutôt que de le faire au coup par coup, surtout si c’est le développeur qui s’en charge.

Mais les désavantages le sont tout autant :

  • ·         le GC peut être plus consommateur de mémoire qu’un mode programmé, surtout au démarrage
  • ·         ses performances vont fortement dépendre de l’application, du nombre d’objets créés et de leur volatilité, ce qui veut dire qu’il ne dédouanera pas totalement le programmeur de la préoccupation mémoire
  • ·         il est difficile de savoir exactement à quel moment un objet pourra être libéré, même si on peut se servir de la classe PhantomReference pour cela. Et il y aura de grandes différences entre les langages faiblement ou fortement typés.

Il paraît difficile de mettre tout le monde d’accord et de dégager une stratégie commune entre ceux qui veulent garder la maîtrise de leur application et de son environnement et qui « détestent » le GC et ceux qui préfèrent se focaliser sur les fonctions métiers, quitte à ne pas être optimal pour ce qui est de la gestion des ressources. Un vieux dilemme que l’on ne résoudra pas plus aujourd’hui qu’hier… Même si on commence à entrevoir un début de tendance…

 

Java n’est pas le seul à bénéficier d’un « garbage collector ». D’autres langages sont dans ce cas, à commencer par ceux de la galaxie .NET, Microsoft parlant alors de code managé.

 

Comment décider de la destruction d’un objet

Le principal écueil qui se dresse devant les développeurs et le « garbage collector », est de définir quand un objet est réputé en « fin de vie » et peut être supprimé.

Au-delà du GC Java, les « ramasse-miettes » pratiquent en gros trois types d’algorithmes : le comptage de références, le mode « traversant » et l’algorithme à générations.

Le comptage de références revient à associer un compteur à chaque objet, mis à jour à chaque fois qu’une référence est créée ou au contraire, disparaît. Quand ce compteur tombe à 0, le GC peut le supprimer.

Compte tenu d’un certain nombre de limites : le temps nécessaire pour gérer les compteurs et les références circulaires qui empêchent de les remettre à 0, par exemple, ce n’est pas la méthode la plus populaire, ni la plus adéquate.

La technique des algorithmes traversants est une manière d’effectuer le nettoyage, en se fondant sur l’arborescence des objets et leurs relations. Ce que l’on peut modéliser en utilisant la célèbre abstraction des trois couleurs de Dijkstra et Lamport, qui date de 1978, un objet blanc étant un objet que le GC n’a pas encore « vu », gris s’il a été vu mais pas encore « traversé » et noir, s’il a été traversé. La différence entre les GC étant la méthode de coloration qui peut être très différente, autrement dit d’affectation d’un état aux objets.

La technique des générations, enfin, mise en pratique dans le G1 Java, revient à hiérarchiser les objets en générations, de la plus jeune à la plus âgée, les données récentes étant habituellement placées dans la génération la plus récente. Ce modèle est généralement construit avec deux ou trois générations et des algorithmes différents pour chacune d’elles. C’est ce qui a été pratiqué avec le GC G1.

Il existe d’autres techniques de GC, concurrentes ou conservatives, par exemple.

 

G1 de Java a été fortement amélioré par rapport aux versions précédentes. Il est beaucoup plus dynamique et réactif, grâce à un découpage plus fin de la « heap » en zones élémentaires.

 

Le G1 de Java

Le Garbage-First de Java (G1) a été introduit, d’abord de manière expérimentale, dans la JVM HotSpot avec la JVM Java SE 6 Update 14, puis comme solution par défaut dans le JDK 9 de septembre 2017. Il est très orienté vers les machines multiprocesseurs, celles qui disposent d’un grand espace mémoire.

Sensiblement plus performant que ses prédécesseurs, G1 a largement fait taire les chefs de projets qui se plaignaient, avec raison, des mauvaises performances des GC Java, au point d’inciter quelques grandes compagnies à écrire leur propre « garbage collector », le plus souvent adapté à un modèle d’application bien précis.

G1 a donc largement amélioré le paysage et a d’ailleurs encore été remanié avec les JDK 10 et 12, pour être plus performant.

Ce qui au passage semble donner raison aux développeurs qui pensent que le nettoyage des objets n’a pas à être traité par le code, car c’est un problème qui concerne l’OS et la machine virtuelle. Pas plus qu’un conducteur de voiture n’a à fabriquer un nouveau carburateur, si celui qui équipe son véhicule ne lui convient pas. A chacun son périmètre de compétence.

Par rapport aux anciennes versions du GC, G1 est organisé différemment. Plutôt que d’avoir trois zones distinctes : « young », « tenured » et « permanent space », il découpe le « heap » en un grand nombre de petites zones, chacune d’elles jouant un rôle bien précis, parmi trois : « old generation » « eden space » et « survivor space », qui ne sont d’ailleurs pas sans rappeler les précédents, à la taille des zones près. En général, la JVM comporte 2 000 zones, chacune d’elles faisant de 1 à 32 MB. Bien entendu, tout cela étant paramétrable.

En termes de fonctionnement, G1 commence toujours par une phase de marquage complète, dans laquelle il détermine si les objets instanciés en mémoire, sont utilisés ou non. Il en profite pour détecter les zones laissées libres.

Cette phase lui permet de concentrer son activité sur les espaces « heap » dans lesquels il y a le plus de zones récupérables. G1 utilise un modèle de prévision de temps de pause (période pendant laquelle les applications sont bloquées), lui-même défini par l’utilisateur dans son paramétrage, la stratégie de compactage et de récupération revenant à se rapprocher au plus près de cette prévision.

En résumé, G1 est un utilitaire de réorganisation de l’espace Heap, avec deux fonctions distinctes : la récupération des zones mémoires occupées par les objets et structures de données, qui ne sont pas référencées (ce qui ne veut pas dire qu’elles ne le seront plus) et le compactage des espaces occupés, de manière à restituer dynamiquement les volumes mémoire les plus conséquents à la JVM et à l’OS, pour un autre usage.

L’expérience montre que G1 est très bien adapté aux applications qui ont besoin de beaucoup de mémoire, au-delà de 5 GB et qui ne veulent pas dépasser des temps d’attente de 0,5 sec. C’est tout au moins ce que recommande Oracle. En se souvenant toujours que dans sa version actuelle, G1 comme les autres GC, n’est pas un outil temps réel.

Globalement on estimera que G1, qui n’est pas obligatoire dans la JVM, se justifie :

·         quand le nombre de « full GC » est trop important

·         quand ces « full GC » sont trop longs et induisent des temps d’attente trop élevés, au-delà de 0,5 sec

·         mais aussi quand le nombre d’objets en mémoire varie fortement pendant une période déterminée

Pour lancer l’exécution de G1, la JVM met plusieurs paramètres à notre disposition : choix de G1, temps de pause maximum en ms (objectif, pas une obligation), ratio entre les tailles des générations new/old, ratio entre les tailles des zones eden/survivor, etc.

Les ingénieurs systèmes ont encore quelques belles années devant eux, pour paramétrer G1 et ses successeurs…

L’avenir

Il est de notre point de vue très clair.

Car le problème n’est pas tant dans la récupération et le compactage des pages mémoires, mais bien dans l’algorithme de « coloriage » des zones pour distinguer les objets selon qu’ils auront été vus, qu’ils ne seront plus référencés, ni référençables, avec des degrés de vraisemblance pour qu’un objet isolé soit de nouveau référencé.

Le lien avec les applications et l’ingénierie de système est évident et l’Intelligence Artificielle devrait jouer un grand rôle dans les GC de demain, qui devraient être capables de modéliser les comportements des applications en termes d’instanciation et de référencement, d’évolution d’usage de la « heap », de réaction en temps réel aux fluctuations des demandes, etc.

Avec la montée en puissance de l’IA, c’est aussi le mode manuel dans le codage qui devrait disparaître, trop lourd, trop dépendant des applications, trop difficile à maintenir en cas de migrations, trop lié aux langages, trop dépendant de la compétence des développeurs… et finalement pas toujours efficace. Ce que contestent bon nombre de programmeurs, avec de justes raisons il est vrai.

L’IA devrait donc prendre la relève et on commence à voir quelques projets sur le sujet, au MIT par exemple, qui n’arriveront à maturité que dans les cinq ans à venir. Ne soyons donc pas trop pressés.