Back to Top

garbage collector et gestion de la mémoire

Le garbage collector et la gestion mémoire dans les programmes

Pour certains développeurs, il peut sembler inutile de chercher à comprendre comment la mémoire se gère dans nos programmes. Après tout, tout se fait tout seul! L’intérêt des langages modernes et relativement haut niveau ne serait-il pas justement de se détacher de cette gestion? Car gérer la mémoire à la main on a déjà essayé en C, mais le PHP, le Python ou le Java c’est quand même vachement plus simple. Cependant il est important de comprendre le fonctionnement des langages que l’on utilise. On va donc essayer de comprendre comment les langages de haut niveau gèrent la mémoire, et en quoi c’est intéressant pour un développeur de le savoir. En bonus, je parlerai rapidement de la gestion de la mémoire en Rust. Cette dernière est particulière et innovante de par l’absence de pointeur explicite et de garbage collector.

Image garbage collector

Pourquoi un Garbage Collector?

Tout d’abord, à quoi sert le Garbage Collector? Il permet tout simplement à nos langages adorés (Python, Java, PHP et j’en passe) de gérer seuls la mémoire. On peut le voir comme un gros aspirateur: quand on l’appel, il nettoie la mémoire des espaces les plus utilisés. C’est tout. Il faut ensuite se dire que l’idée centrale derrière la nécessité d’une gestion mémoire, c’est sa finitude. En effet, impossible de garder en mémoire des informations de manière infini, à un moment, la mémoire est pleine. Et puis n’oubliez pas qu’il n’y a encore pas si longtemps, nos PC n’étaient pas des plus performants! Enfin bref, toutes les valeurs manipulées par nos programmes sont stockées en mémoire vive, ce qui est loin d’être un espace immense, en plus d’être plutôt cher. Heureusement pour nous, il est vraiment peu probable que toutes les données soit manipulées en même temps. Il est donc intéressant de chercher un mécanisme qui pourrait détecter les ressources les plus utilisées pour libérer la mémoire et laisser de la place.

Comment est gérée la mémoire?

Alors comment faire pour déterminer quand libérer de la mémoire, et quelle variable supprimer? Pour les plus théoriciens, je vous renvoie au théorème de Rice. Celui-ci démontre que cette détermination est indécidable en « lisant » uniquement le code. Pour cet article, je vais surtout prendre en exemple le garbage collector de Java, d’une part car c’est le plus connu, et aussi parce que c’est celui avec le plus de ressources disponibles pour le comprendre. Le garbage collector (pour Java du moins) fonctionne sur un système de génération. Cela permet d’avoir une gestion différente pour les objets selon le temps qu’ils ont passé en mémoire. On a donc une mémoire fragmentée en différents segments, un pour chaque tranche de vie des objets en mémoire. Une pour les objets jeunes et une pour les plus vieux. A noter qu’un autre espace, pour les objets permanents, existait jusqu’à Java8, mais a été supprimé.

Tout d’abord, les objets sont initialisés dans le segments YoungGeneration. A chaque fois qu’un objet est nécessaire à notre programme, un compteur s’incrémente, et lorsque l’objet ne devient plus utile il se décrémente. Ainsi un objet avec un compteur a 0 n’est plus utile et peut être supprimé. Lorsque notre zone YoungGeneration devient pleine, on lance une Garbage Collection: les objet avec un compteur à zéro sont supprimés, et les éléments restant vieillissent d’une génération. La majorité des objets ont une durée de vie très courte. Ainsi, lors de cette garbage collection, beaucoup d’objets ont leur compteur à zéro.
Il est donc intéressant d’optimiser cette partie de la mémoire pour les objets à faible durée de vie. Pour cette optimisation, la zone est découpée en un Eden (zone ou tous les éléments naissent) et deux espaces plus petits de même taille, où tous les survivants sont stockés alternativement, sorte de sas avant la promotion en OldGeneration. 

Schéma garbage collector

Notons que si l’espace demandé est trop grand pour aller dans une zone de survivant, il est directement alloué dans la zone OldGeneration, quel que soit son âge.

Avant passage sur YoungGeneration:

Schéma explicatif garbage collector avant

Et après:

Schéma explicatif garbage collector après

Passé un certain âge, les éléments toujours utilisés sont transférés vers la zone OldGeneration. Lorsque la zone OldGeneration est pleine, on lance une Full Garbage Collection. On fait une Garbage Collection classique sur la YoungGeneration mais on vide aussi la OldGeneration des éléments avec un compteur à zéro. Ce cycle complet de garbage collection est cependant beaucoup plus long qu’une garbage collection simple. En effet, la zone à nettoyer est tout simplement plus grande.

Pourquoi comprendre le fonctionnement du Garbage Collector?

Alors savoir tout ça, c’est cool, mais on ne contrôle rien, donc on s’en fiche non? Alors deux choses: d’une part on peut plus ou moins le contrôler, et surtout, savoir comment ça fonctionne nous aide à rendre nos programmes plus performants. En Java (en C# aussi d’ailleurs, pas de sectarisme chez FoutuCode), on peut explicitement appeler le garbage collector. Bon ce n’est pas vraiment recommandé, parce que on lui dit assez grossièrement « s’il te plait, ça serait cool que tu passes ». Sauf qu’il peut tout autant accourir qu’avoir la flemme. En d’autres termes rien ne garantit que le Garbage Collector passe directement après son appel. Pire encore, son appel provoque un cycle Full Garbage Collection, très long.

Par contre, on sait maintenant que multiplier les objets à faible durée de vie est une mauvaise idée. Ils vont devoir être éliminés très vite, multipliant les appels au Garbage Collector, qui peuvent prendre un certain temps. On augmente donc l’âge des autres objet en les déplaçant vers la OldGeneration. Le fonctionnement du Garbage Collector nous pousse donc à réutiliser au maximum nos structure de données. Par exemple, en vidant une structure inutilisée pour la remplir à nouveau au lieu d’en initialiser une nouvelle.
Pour faire simple: arrêtez de créer des centaines de variables ou fonctions qui ne servent à rien et optimisez!

Rust et la gestion de la mémoire

Parlons désormais de Rust. La mémoire est ici gérée d’une manière totalement nouvelle, puisqu’elle est gérée seule, mais sans garbage collector. Les variables vivent dans une portion de code limitée et la portion de mémoire n’est allouée qu’à une entité à la fois, leur libération est donc triviale: hors de ce segment de code, la variable n’existe plus. Ce mécanisme extrêmement puissant permet de libérer la mémoire au fur et à mesure de l’exécution. 

fn main() {
  let x = String::from("Hello World!");
  let y = x;
  println!("{}", x); //non ok, car x
  println!("{}", y); //ok  
}

Dans cet exemple, la mémoire occupé par X est libérée à l’instant ou Y récupère les « droits » mémoire. On se passe ainsi de garbage collector.

Pour conclure

Le comptage de référence est la technique historique des Garbage Collector. Cependant cette technique ne permet pas, par exemple de détecter les références circulaires. C’est pourquoi, en pratique, des algorithmes basés sur une représentation sous forme de graphe sont préférés. J’ai néanmoins préféré parler du comptage de références, plus didactique et facile à comprendre. L’intérêt de l’article étant essentiellement de comprendre l’intérêt pour les développeurs, vous me pardonnerez, je l’espère, ces quelques simplifications.
Étant donné le sujet, on a pas de memes en stocks … Donc je vous laisse voir directement nos derniers memes!

Par Clément Caffin


Random_content()

Rechercher:





Suis-nous sur les réseaux sociaux!

Instagram

S'abonner