Stack, heap et garbage collector
Se représenter la pile, le tas et le ramasse-miettes.
Objectifs
À la fin de cette leçon, vous saurez :
- vous représenter les deux zones de mémoire d'un programme JavaScript : pile et tas ;
- situer valeurs primitives, références et objets dans ces zones ;
- expliquer ce que fait le garbage collector et quand il intervient ;
- reconnaître les écritures qui retiennent la mémoire inutilement (fuites).
🔗 Pour vous rafraîchir la mémoire : objets et références
Pourquoi ce modèle ?
Les deux leçons précédentes ont introduit un vocabulaire précis — valeur copiée, référence partagée — sans dire où tout cela vit physiquement. Ce modèle final boucle la boucle : il donne une place concrète à chaque acteur, et explique des phénomènes réels (lenteurs, plantages « out of memory ») que vous rencontrerez en production.
Souvenez-vous du chapitre 2 : un processus dispose de RAM. JavaScript organise cette RAM en deux grandes zones aux règles opposées :
STACK (la pile) HEAP (le tas)
────────────────────────── ──────────────────────────
petite, ultra rapide grande, souple
organisée en piles d'assiettes organisée librement
contient : contient :
- valeurs primitives - objets
- références (adresses) - tableaux
- contextes d'exécution - fonctions
nettoyage AUTOMATIQUE nettoyage par le
à la sortie de chaque fonction GARBAGE COLLECTOR
L'image de la pile : une pile d'assiettes. On pose un contexte au sommet à chaque appel de fonction ; on l'enlève à son retour. Dernier posé, premier retiré, sans exception — c'est ce qui la rend si rapide.
Le tas n'a pas cette discipline : les objets y sont posés où il y a de la place, pour une durée imprévisible. Cette liberté a un prix : quelqu'un doit venir faire le ménage — c'est le garbage collector.
Suivre un programme dans la mémoire
function exemple() {
let n = 42; // primitive : valeur dans la stack
const user = { age: 30 }; // référence dans la stack → objet dans le heap
return user;
}
const garde = exemple();
Déroulé complet :
1. appel d'exemple()
┌ STACK ─────────────┐ ┌ HEAP ────────────┐
│ exemple: │ │ │
│ n = 42 │ │ (vide) │
└────────────────────┘ └──────────────────┘
2. création de l'objet
┌ STACK ─────────────┐ ┌ HEAP ─────────────┐
│ exemple: │ │ { age: 30 } ←──┐ │
│ n = 42 │ │ │ │
│ user = ●─────────┼──────┼────────────────┘ │
└────────────────────┘ └───────────────────┘
la flèche EST la référence stockée dans la stack
3. return user : la référence est COPIÉE vers l'appelant,
puis le contexte d'exemple() est DÉPILÉ
4. après le retour
┌ STACK ─────────────┐ ┌ HEAP ─────────────┐
│ garde = ●──────────┼──────→ { age: 30 } │
└────────────────────┘ └───────────────────┘
n a disparu avec la stack frame.
L'objet SURVIT : quelque chose (garde) pointe encore lui.
Ce déroulé rassemble tout le chapitre : la variable locale n meurt avec sa fonction ; l'objet survit tant qu'une référence vivante le vise — peu importe où elle habite.
Le garbage collector : le jardinier du tas
Qui libère les objets du tas ? Personne ne fait de free() en JavaScript : c'est le ramasse-miettes (garbage collector, GC), qui parcourt périodiquement le heap et libère tout objet devenu inatteignable — c'est-à-dire qu'aucune chaîne de références vivantes ne permet plus d'y accéder depuis le programme.
let panier = { items: ["pomme"] };
panier = null;
// l'objet { items } n'a plus AUCUNE référence qui le vise :
// il devient inatteignable → candidat au ramassage (au prochain passage)
Trois propriétés à bien comprendre :
- Rien n'est immédiat. Entre la perte de la dernière référence et la libération effective, un temps indéterminé s'écoule (le GC choisit ses moments pour ne pas trop gêner). On ne contrôle pas ce rythme — et on n'en a généralement pas besoin ;
- Atteignable = conservé, même inutile. Un objet référencé par une globale oubliée sera JAMAIS collecté, même si plus aucune ligne du programme ne l'utilise. Le GC raisonge accessibilité, pas utilité ;
- La mémoire est finie. Tout ce qui s'accumule sans être libéré finit par peser : c'est l'origine des fuites.
Les fuites : retenir ce qui devrait partir
Une fuite de mémoire survient quand le programme conserve des références vers des données dont il ne se servira plus. Exemple canonique :
const journal = []; // global, donc atteignable à vie
function traiter(donnees) {
journal.push(donnees); // on garde TOUT, pour toujours
}
traiter({ page: 1 });
traiter({ page: 2 });
// ... dix mille appels plus tard : le tableau grossit sans limite
Analysons avec nos nouveaux outils : journal est une globale → toujours atteignable → jamais collectée. Chaque push y accroche une référence de plus. Rien n'est « cassé » au sens des erreurs : le programme fonctionne parfaitement... jusqu'à épuiser la mémoire, souvent après des heures ou des jours. C'est ce qui rend les fuites redoutables : elles ne se voient pas en développement, elles explosent en production.
Autres sources classiques (rencontrées dans les chapitres suivants) :
- écouteurs d'événements ajoutés mais jamais retirés (chapitre navigateur) ;
- minuteurs (
setInterval) jamais annulés ; - caches qui grossissent sans politique d'éviction.
L'antidote universel tient en une question : « qui peut encore atteindre cet objet ? » Si la réponse est « personne ne devrait », coupez la référence (remplacez-la, retirez-la, bornez la structure).
Version corrigée du journal, bornée :
function traiter(donnees) {
journal.push(donnees);
if (journal.length > 100) {
journal.shift(); // jette le plus ancien : taille maximale constante
}
}
La mémoire consommée plafonne désormais, quel que soit le nombre d'appels.
Ce qu'il faut retenir — et ce qu'il ne faut PAS en conclure
À retenir :
- primitives et références vivent dans la stack, objets dans le heap ;
- la stack se nettoie seule au retour des fonctions ; le heap dépend du GC ;
- atteignable = conservé ; inatteignable = collectable ;
- une fuite = une référence conservée vers des données mortes.
À ne PAS en conclure : qu'il faut optimiser la mémoire à chaque ligne. Le moteur fait très bien son travail tant que vos structures restent simples. La compétence visée ici est diagnostique : quand un processus grossit anormalement, vous saurez poser la bonne question (« quelle référence retient quoi ? ») au lieu de deviner au hasard.
Exercice
- Pour chaque ligne, indiquez stack / heap / les deux :
let total = 0;
const titres = ["a", "b"];
const meta = { taille: 2 };
function creer() {
const local = { valeur: 123 };
return local;
}
const garde = creer();
L'objet survit-il à la fin de la fonction ? Justifiez avec le mot « atteignable ».
-
Même question sans la ligne
const garde = creer();. Que devient l'objet, et quand exactement ? -
Expliquez pourquoi le tableau
journaln'est jamais collecté alors que ses éléments sont vieux. -
Modifiez
traiterpour ne conserver que les 100 derniers éléments. -
Question de synthèse : reliez les trois leçons du chapitre en une phrase chacune — copie par valeur, références, stack/heap.
Résumé
- Stack : petite, rapide, empilée par appel de fonction ; primitives + références ; nettoyage automatique au retour.
- Heap : vaste, libre ; objets, tableaux, fonctions ; nettoyé par le GC.
- Le GC libère ce qui est devenu inatteignable — pas ce qui est devenu inutile.
- Une fuite = référence tenue vers des données mortes ; antidote : borner et couper.
- Le modèle sert au diagnostic, pas à la micro-optimisation.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Exercice 1.
total : stack (primitive)
titres : LES DEUX — la référence dans la stack, le tableau ["a","b"] dans le heap
meta : idem — référence (stack) + objet { taille: 2 } (heap)
La réponse complète exige de distinguer la variable (toujours stack) de la donnée pointée (heap si objet). Cette double lecture est le réflexe à installer.
Exercice 2. Oui, il survit. Au return, la référence est copiée hors de la fonction (dans garde) avant que le contexte ne soit dépilé : une chaîne de références vivante relie le programme à l'objet → il reste atteignable → le GC ne le touchera jamais. C'est exactement le passage par valeur appliqué aux valeurs de retour : la valeur retournée était une adresse, l'adresse voyage.
Exercice 3. Sans garde, la dernière référence meurt avec la stack frame : l'objet devient inatteignable dès le retour de la fonction. Sa libération physique, elle, attendra un prochain passage du garbage collector — instant non déterminable, et d'ailleurs inintéressant : le résultat observable (mémoire récupérable) est identique.
Exercice 4. Parce que « vieux » n'a aucun sens pour le GC : seul compte « atteignable ». journal étant global, toute donnée poussée dedans reste reliée au programme par une chaîne de références intacte. Le GC ne peut pas deviner qu'un élément ne servira plus — personne ne le lui a dit.
Question 5.
function traiter(donnees) {
journal.push(donnees);
if (journal.length > 100) {
journal.shift(); // retire le plus ancien
}
}
Variante équivalente : while (journal.length > 100) journal.shift();. L'essentiel : une règle explicite borne la croissance. Toute structure d'accumulation durable mérite sa borne — c'est une règle de production, pas un exercice scolaire.
Question 6. Réponse attendue (une phrase par leçon, formulations libres) :
- Copie par valeur : les primitives se dupliquent intégralement, donc leurs copies sont étanches.
- Références : les objets se partagent par adresse copiée, donc mutations visibles par tous les porteurs.
- Stack/heap : variables et adresses vivent sur la pile (nettoyée au retour des fonctions), les objets survivent dans le tas tant qu'une référence les rend atteignables.