Théorie
1. What is a garbage collector?
Le garbage collector libère automatiquement les objets qui ne sont plus reachable. Il commence par un ensemble de roots — par exemple le global object et les variables des fonctions en cours — puis suit chaque reference.
Vocabulary
- reference
- Le lien qu’un objet garde vers un autre objet.
- reachable
- Un objet auquel une root permet encore d’arriver en suivant des references.
- unreachable
- Un objet qu’aucune root ne peut atteindre : le garbage collector peut le récupérer.
- island
- Un groupe d’objets qui se pointent entre eux. Il peut être reachable ou unreachable ; il n’est pas un memory leak par lui-même.
B ↔ C est lui aussi reachable depuis la root.En JavaScript, c’est différent. Le garbage collector suit les references depuis les roots, au lieu de compter les references entrantes. L’island B ↔ C est unreachable après unlink : elle est donc collectible. Un cycle seul n’est pas un memory leak.
Mémo de lecture
2. Stack et heap : deux espaces à distinguer
- Le stack contient un stack frame pour chaque appel de function en cours.
- Un stack frame garde les variables et references locales de cet appel (bindings), ainsi que l’endroit où l’exécution doit reprendre.
- Le heap contient les objects créés.
- Quand l’appel se termine, son stack frame quitte le stack ; les objects du heap ne disparaissent que lorsqu’aucune root ne peut plus les atteindre.
00-basic/src/memory-leak.jsLe cas qui fuit vraiment
3. What is a typical memory leak?
Un memory leak apparaît lorsqu’un objet devenu inutile reste malgré tout reachable. Le programme a terminé avec lui, mais une root conserve encore — souvent indirectement — une reference vers cet objet.
window.memoryLeakLab
myClumsyArray[]
B ↔ C
Voici le modèle à étudier, sans la plomberie Vue ni les boutons. Il construit, détache et retient les objects du lab.
00-basic/src/memory-leak.jsconst PAYLOAD_SIZE = 1024 * 1024 * 8;
export const BATCH_PAYLOAD_SIZE = 1024 * 8;
let nextId = 1;
// Liste applicative oubliée / Forgotten application list.
export const myClumsyArray = [];
export class Brol {
constructor(id, name, payloadSize = PAYLOAD_SIZE) {
this.name = `${name}-${id}`;
this.peer = null;
this.payload = new Array(payloadSize).fill(id);
}
}
export class StructureA {
constructor(id, b, c) {
this.name = `A-${id}`;
this.b = b;
this.c = c;
}
}
export function buildStructure(payloadSize = PAYLOAD_SIZE) {
const id = nextId++;
const b = new Brol(id, 'B', payloadSize);
const c = new Brol(id, 'C', payloadSize);
b.peer = c;
c.peer = b;
return new StructureA(id, b, c);
}
export function unlinkStructure(a) {
a.b = null;
a.c = null;
}
export function leakStructure(a) {
myClumsyArray.push({ id: a.name, b: a.b, c: a.c });
unlinkStructure(a);
}
Ici, le cycle n’est toujours pas le problème. C’est window.memoryLeakLab.myClumsyArray, une liste que nous avons oublié de vider, accessible depuis une root, qui empêche garbage collection.
Dans une application réelle, cette liste pourrait être :
- une history undo/redo qui retient les objects complets au lieu d’un identifiant ou d’un snapshot plus léger ;
- des entrées d’audit qui gardent une reference directe vers l’object audité ;
- un cache, une liste de debug ou un tableau « temporaire » que nous avons simplement oublié de vider.
Laboratoire
4. Vérifier qu’une structure devient collectible
Avant de chercher un memory leak, vérifions le comportement normal. Une seule structure suffit pour rendre le phénomène visible dans le JS heap.
Deux tailles, deux objectifs : Create 1 structure utilise PAYLOAD_SIZE pour rendre un objet volumineux immédiatement visible. Make 10,000 clumsy entries utilise volontairement BATCH_PAYLOAD_SIZE, beaucoup plus petit : le test doit révéler une reference retenue, pas afficher la page de crash du navigateur.
Pourquoi ×8 ?
On se lance !
- Lisez
buildStructure(). Oui, il fait exactement ce qu’il faut : il crée B et C, les relie, puis les place sous A. - Cliquez Create 1 structure et observez le compteur de JS heap monter.
-
Lisez
unlinkStructure()dans le controller. Il retire les references de A vers B et C, puis le controller metcurrentAànull: la root ne garde plus la structure.
00-basic/src/lab-controller.js - Cliquez Unlink structure. B et C peuvent encore se pointer mutuellement, mais ils sont désormais unreachable et donc collectible.
-
Si le compteur ne redescend pas immédiatement, ouvrez DevTools, puis Memory, et cliquez l’icône de garbage collection dans la barre d’outils. Le navigateur décide normalement du moment de ce travail ; ici, nous demandons une mesure comparable.
DevTools → Memory → icône de garbage collection
Avant : une structure volumineuse est encore visible.
Après : le heap revient près de son niveau initial.
Le compteur du JS heap donne une tendance et garbage collection peut le faire varier. La preuve finale reste un retaining path depuis une root, pas la hausse ou la baisse seule du compteur.
Laboratoire
5. Oops, I leak it again
Cette fois, cliquez Make 10,000 clumsy entries. Chaque entrée est légère, mais elle garde des references vers B et C dans myClumsyArray. Après unlinkStructure(), ces objects ne sont donc pas collectible : la liste oubliée les retient encore.
Dans une application réelle, cette liste pourrait être un historique d’undo/redo, des entrées d’audit, ou un tableau « temporaire » jamais vidé. Comme le dit l’adage : il n’y a que le provisoire qui dure.
- Observez le compteur et le nombre d’entrées de
myClumsyArrayaugmenter. - Dans un heap snapshot, suivez le retaining path de B ou C jusqu’à
window.memoryLeakLab.myClumsyArray. - Cliquez Empty my clumsy array, puis relancez garbage collection : cette fois, la root a vraiment lâché les objects.
Dans la suite, nous allons explorer une technique simple à apprendre, étonnamment sous-utilisée et redoutablement convaincante : prouver exactement ce qui retient un object. De quoi arriver à votre prochaine mission avec des preuves — et quelques bragging rights bien mérités.