Ramasse-miettes
Le ramasse-miettes, ou le «Garbage Collection» en anglais, est une ressource complète dédié à la gestion automatique de la mémoire dynamique. Elle permet de réutiliser les parties de mémoires libérer par le ou les programmes afin que d'autres programmes puis l'utiliser selon leur besoin. Il s'agit en quelque sorte d'une forme de recyclage de la mémoire. Ainsi, si on utilise pas de ramasse-miettes pour gérer la mémoire, un programme à la possibilité de soit gérer manuellement l'espace de mémoire dynamique qu'il dispose ou gérer sa mémoire comme un vulgaire tableau incrémentale où chaque morceaux libéré ne sera jamais réutilisé. Cette dernière n'est pas vraiment pratique, car plus vous utiliseriez un programme plus il consommerait de la mémoire et finirait nécessairement par toute l'utiliser et un redémarrage du système serait donc nécessaire pour réutiliser cette mémoire.
Le premier système proposant un ramasse-miettes de façon standard dans son environnement fut le langage de programmation Lisp inventé en 1958 par John McCarthy et le premier programme de ramasse-miette fut écrit par son étudiant Daniel J. Edwards. Tandis que du côté francophone, la première fois que le terme ramasse-miettes fut utilisé plutôt que le terme anglophone «Garbage Collection», date de 1970 dans les communications de Claude Pair (mathématicien, informaticien et haut fonctionnaire français) pour son cours d'informatique à l'université de Nancy en France.
Les algorithmes
| Nom | Description |
|---|---|
| Algorithme à comptage de références | Cet algorithme permet de maintenir, pour chaque objet, le nombre de référence d'un objet. Ainsi, s'il devient nulle, alors il peut être recyclé. |
| Algorithme traversants | Cet algorithme traverse un graphe d'objets accessible à partir de la racine pour distinguer les objets étant accessible (actuellement utilisé) par rapport aux objets étant inaccessible (plus utilisé, donc recyclable). Ils sont généralement séparés eu même en deux algorithmes : algorithme marquants et nettoyants (appelé mark and sweep collector en anglais) et algorithme copiant (appelé copy collector en anglais). |
| Algorithmes à générations | Cet algorithme permet d'introduire, une forme de durée de vie (génération) des objets dans les différentes zones. Ainsi, on aura une zone de jeune génération d'objets (appelé Young Generation en anglais) et une zone de vieille génération ou ayant une durée de vie plus longue (appelé Old Generation ou Tenured Generation en anglais). C'est au moment du nettoyage (collection), qu'est déterminé s'il finira pas être séparé de la jeune génération pour aller dans la vieille génération, car si l'objet survit à plusieurs nettoyage de la mémoire, c'est qu'il est considéré comme ayant une durée de vie plus longue. Ainsi, on réduira le nombre de nettoyage majeure (collections majeures) de cette façon. |
Algorithme à comptage de références
L'algorithme à comptage de références est l'une des méthodes les plus simples et les plus utilisées pour la gestion automatique de la mémoire dans les langages de programmation. Chaque objet en mémoire est associé à un compteur indiquant combien de références pointent vers lui. Lorsqu'une nouvelle référence est créée, ce compteur est incrémenté, et lorsqu'une référence est supprimée ou redirigée, le compteur est décrémenté. Si ce compteur atteint zéro, cela signifie que plus aucun code du programme ne peut accéder à cet objet, il est donc automatiquement libéré. Ce mécanisme permet une libération immédiate et prévisible de la mémoire, sans avoir besoin d'un balayage périodique de la mémoire de tas. Cette méthode est souvent intégrée dans les langages qui visent une gestion fine et localisée de la mémoire, comme Python ou Swift. Elle offre l'avantage de fonctionner en temps réel, sans pauses longues.
Sur le plan des structures de données, cet algorithme s'intègre naturellement dans les systèmes à base de graphes, listes chaînées, arbres, ou objets interconnectés. À chaque liaison entre deux structures, une référence est créée et comptabilisée. Lorsqu'une structure est supprimée ou perdue, toutes les références sont mises à jour automatiquement, et si certaines deviennent inaccessibles, la libération en cascade peut s'opérer. Toutefois, ce comportement est dépendant de l'exactitude du comptage, et un oubli ou une erreur peut conduire à des fuites mémoire. L'algorithme est facile à implémenter mais nécessite que toutes les modifications de pointeurs soient correctement gérées, ce qui peut ralentir légèrement les performances. Dans les systèmes embarqués ou les applications à temps réel, son usage est apprécié pour sa prévisibilité. Il fonctionne aussi bien dans un environnement orienté objet que dans des structures plus classiques.
Cependant, l'une des principales limites du comptage de références réside dans son incapacité à gérer les cycles de références. Par exemple, deux objets se référant mutuellement verront leurs compteurs rester au-dessus de zéro même s'ils ne sont plus accessibles par le programme. Ces objets deviennent donc incollectables par cet algorithme seul, ce qui conduit à des fuites de mémoire. Pour pallier ce défaut, certains environnements intègrent un second ramasse-miettes basé sur la recherche d'objets inaccessibles (comme le marquage/balayage) en complément du comptage. Ainsi, on combine la réactivité du comptage de références avec la couverture plus complète d'un collecteur cyclique. En résumé, bien que très utile et efficace dans de nombreux cas, le comptage de références doit souvent être renforcé par d'autres stratégies pour garantir une gestion mémoire complète et robuste. C'est pourquoi il constitue un bon compromis dans des systèmes de taille moyenne ou avec une gestion contrôlée des cycles.
|
MODULE ComptageDeRéférences BOUCLE POUR CHAQUE objet alloué dans le programme FAIRE Initialiser un compteur de références à 0 FIN BOUCLE POUR CHAQUE SI nouvelle référence vers un objet est créée ALORS Incrémenter le compteur de références de cet objet FIN SI SI référence vers un objet est supprimée ou remplacée ALORS Décrémenter le compteur de références de cet objet SI compteur = 0 ALORS Libérer l'objet de la mémoire BOUCLE POUR CHAQUE objet référencé par cet objet FAIRE Supprimer également cette référence (récursivement) Décrémenter leurs compteurs de références Répéter la libération si nécessaire FIN BOUCLE POUR CHAQUE FIN SI FIN SI |
Algorithme traversants
L'algorithme traversant, souvent associé à la technique de marquage et balayage (mark and sweep), est une méthode de gestion mémoire reposant sur l'identification des objets accessibles à partir d'un ensemble de références appelées racines. Ces racines incluent généralement les variables globales, la pile d'appels et les registres. L'algorithme explore récursivement tous les objets atteignables depuis ces racines en les marquant comme vivants. Une fois cette phase de marquage terminée, tous les objets non marqués sont considérés comme inaccessibles et donc candidats à la libération. Ce processus permet de récupérer efficacement la mémoire occupée par les objets inutilisés. Le principal avantage de cette méthode est sa capacité à gérer les cycles de références, ce que les algorithmes à comptage de références ne peuvent pas faire. Elle est donc particulièrement utile dans des structures de données complexes où des objets peuvent se référencer mutuellement.
Lors de la traversée, l'algorithme visite les objets à partir des racines et suit chaque champ qui contient une référence vers un autre objet. Il fonctionne sur le principe de la reconnaissance de graphe orienté, où chaque noud (objet) est exploré de façon récursive ou itérative. La structure de données utilisée est souvent une pile ou une file pour gérer les objets à visiter. À chaque visite, l'objet est marqué pour éviter les répétitions et garantir qu'il ne sera pas libéré à tort. La complexité temporelle dépend de la taille du graphe d'objets vivants, mais elle est généralement acceptable pour des programmes bien structurés. Les structures comme les listes chaînées, les arbres et les graphes se prêtent bien à cette approche, car elles peuvent être parcourues sans ambiguïté à partir des racines. Ce mode de fonctionnement est plus global que le comptage de références, car il traite la mémoire de manière cohérente en un seul passage périodique.
L'inconvénient majeur de l'algorithme traversant est qu'il nécessite un arrêt du programme pendant la phase de collecte, ce qu'on appelle le stop-the-world. Cela peut provoquer des latences visibles dans les applications temps réel ou interactives, où la fluidité est cruciale. Pour pallier cela, des variantes modernes comme les collecteurs incrémentaux ou concurrentiels ont été développées. Malgré cette limitation, l'approche traversante reste très fiable et permet une gestion mémoire sans fuite, même dans des graphes d'objets très complexes. Elle est largement utilisée dans des environnements comme Java ou .NET, où la mémoire est abondante mais doit être contrôlée de façon sûre. En combinant simplicité conceptuelle et robustesse, l'algorithme traversant est une solution adaptée pour les systèmes à structures de données riches et fortement interconnectées. Il offre une base solide pour des améliorations plus sophistiquées en gestion automatique de la mémoire.
|
MODULE RamasseMiettes_Traversant * Phase de marquage : on identifie tous les objets accessibles BOUCLE POUR CHAQUE objet dans mémoire FAIRE objet.marqué ← FAUX FIN BOUCLE POUR CHAQUE pile ← nouvellePile() BOUCLE POUR CHAQUE racine dans racines FAIRE empiler(pile, racine) FIN BOUCLE POUR CHAQUE TANT QUE pile n'est pas vide FAIRE objet ← dépiler(pile) SI objet.marqué = FAUX ALORS objet.marqué ← VRAI BOUCLE POUR CHAQUE référence dans objet.références FAIRE empiler(pile, référence) FIN BOUCLE POUR CHAQUE FIN SI FIN TANT QUE * Phase de balayage : on libère les objets non marqués BOUCLE POUR chaque objet dans mémoire FAIRE SI objet.marqué = FAUX ALORS libérer(objet) FIN SI FIN POUR |
Algorithmes à générations
L'algorithme à générations est une stratégie de ramasse-miettes fondée sur une observation clef du comportement des programmes : la majorité des objets alloués en mémoire ont une durée de vie courte. Pour optimiser la gestion de la mémoire, cet algorithme divise l'espace mémoire en plusieurs zones appelées générations, généralement au nombre de trois?: la jeune génération, la génération intermédiaire (ou adulte), et la génération ancienne. Les objets nouvellement créés sont d'abord placés dans la jeune génération, qui fait l'objet de collectes fréquentes mais rapides. En cas de survie à plusieurs cycles de collecte, ces objets sont promus vers des générations plus âgées. Cela permet de concentrer l'effort du ramasse-miettes sur les zones où le taux de libération est élevé, tout en évitant de revisiter inutilement les objets durables.
La collecte dans les générations est déclenchée de manière différenciée. La jeune génération est nettoyée souvent, car elle contient surtout des objets temporaires, ce qui permet une collecte efficace et rapide. En revanche, les générations adultes et anciennes sont nettoyées moins fréquemment, car elles sont supposées contenir des objets vivants sur le long terme. Cette hiérarchisation permet d'éviter les performances dégradées d'une collecte intégrale à chaque allocation. Le processus de promotion des objets se fait en fonction d'un âge logique, calculé en fonction du nombre de cycles de collecte qu'un objet a traversé sans être supprimé. Lorsqu'un objet atteint un seuil d'âge, il est déplacé vers la génération suivante, où la fréquence de collecte est plus basse.
L'utilisation d'un algorithme à générations améliore significativement les performances du ramasse-miettes, notamment dans les langages à gestion automatique de mémoire comme Java, .NET ou certains moteurs PHP. Il diminue la latence globale des programmes en réduisant la fréquence des collectes complètes, et en rendant plus prévisibles les pauses dues au nettoyage mémoire. En revanche, cette méthode introduit une complexité supplémentaire dans la gestion des objets intergénérationnels, notamment les pointeurs d'objets jeunes depuis les objets anciens, qu'il faut surveiller par des write barriers (barrières d'écriture). Malgré ces défis techniques, les algorithmes à générations sont devenus une norme dans la conception moderne des collecteurs de mémoire. Ils permettent de concilier efficacité, robustesse et bon usage des ressources dans des applications de grande envergure.
|
MODULE allouerObjet(objet) Placer objet dans GénérationJeune MODULE collecterGenerationJeune() Marquer tous les objets accessibles depuis les racines dans GénérationJeune Balayer et supprimer les objets non marqués dans GénérationJeune BOUCLE POUR CHAQUE objet survivant FAIRE Incrémenter le compteur d'âge SI compteur d'âge > seuil ALORS Promouvoir objet dans GénérationAdulte FIN SI FIN BOUCLE POUR CHAQUE MODULE collecterGenerationAdulte() Marquer tous les objets accessibles depuis les racines + jeunes promotions Balayer et supprimer les objets non marqués dans GénérationAdulte Promouvoir les objets anciens vers GénérationAncienne si nécessaire MODULE collecterGenerations() collecterGenerationJeune() SI mémoire faible ou seuil atteint ALORS collecterGenerationAdulte() FIN SI * Appel principal périodique ou conditionnel collecterGenerations() |
Ramasse-miettes par langage de programmation
Voici un tableau récapitulatif des algorithmes de ramasse-miettes (ou de gestion mémoire) utilisés dans différents langages de programmation populaires. Ce tableau mentionne les algorithmes principaux, mais garde à l'esprit que certains langages ou environnements peuvent combiner plusieurs stratégies ou en proposer plusieurs options selon les versions ou les implémentations.
| Langage | Comptage de Références | Traversants (Mark & Sweep, Mark & Compact) | Générations | Autres Algorithmes / Particularités |
|---|---|---|---|---|
| Ada | Oui | Non | Non | Généralement pas de GC, gestion manuelle ou bibliothèque tierces |
| C | Non | Non | Non | Allocation/désallocation manuelle (malloc/free) |
| C++ | Oui (via smart pointers) | Non | Non | RAII, unique_ptr, shared_ptr |
| C# (.NET) | Non (rare) | Oui | Oui | Server GC, Workstation GC, Background GC |
| Dart | Non utilisé | Oui (Mark & Sweep compactant) | Oui | Utilise un GC par générations (new space / old space), optimisé pour les applications Flutter. En mode Dart VM (JIT), le GC est compilant à chaud. |
| Delphi | Oui | Non | Non | Référencement cyclique non détecté |
| Free Pascal | Non (par défaut) | Non | Non | Gestion mémoire manuelle (FreeMem, GetMem,...), possibilité de bibliothèque externe |
| GNU C | Oui, utilisé avec des bibliothèques externes (par exemple, Boehm GC) | Oui, via des implémentations tierces comme Boehm GC (Mark & Sweep, Mark & Compact) | Non utilisé explicitement | GNU C n'a pas de ramasse-miettes intégré ; les bibliothèques externes comme Boehm GC ou libgc gèrent la collecte. Elles sont adaptées à des programmes plus complexes ou en gestion mémoire manuelle. |
| Go | Non utilisé | Oui (Mark & Sweep, Stop-the-world optimisé) | Non utilisé explicitement | Utilise un GC concurrent et incrémental, à faible pause ; très optimisé pour les goroutines et le parallélisme. |
| Haskell (GHC) | Non | Oui | Oui | Generational GC avec compactage, lazy evaluation supporté |
| Java | Non (rare) | Oui (Mark & Sweep, Mark & Compact) | Oui | G1 GC, ZGC, Shenandoah GC, CMS |
| JavaScript (V8) | Non | Oui | Oui | Mark & Sweep, Mark & Compact, Incremental GC |
| Rust | Oui (RAII) | Non | Non | Pas de GC automatique, mémoire gérée statiquement |
| Swift | Oui (ARC) | Non | Non | Automatic Reference Counting (inspiré d'Objective-C) |
| Turbo Pascal | Non | Non | Non | Gestion mémoire manuelle (GetMem/FreeMem) |
| Visual Basic (VB6) | Non | Oui | Non | Mark & Sweep simple, sans génération |
| VB.NET | Non | Oui | Oui | Repose sur le GC de .NET |
Bibliothèque de ramasse-miette
Voici un tableau répertoriant quelques bibliothèques de ramasse-miettes (garbage collection) pour différents langages de programmation :
| Langage | Bibliothèque |
|---|---|
| GNU C | Boehm GC |
| Haskell | GHC/Memory Management |
| PHP | Zend GC | /tr>
| Swift | Automatic Reference Counting (ARC) |