02 - Le cycle de vie de la mémoire
Ce que tu vas apprendre
- Les trois phases : allocation, utilisation, libération
- La différence entre allocation explicite et implicite
- Ce que "garbage" veut dire (objets inaccessibles)
- Le concept d'accessibilité (reachability)
Prerequisites
- Avoir lu Stack vs Heap
- Comprendre la différence entre primitives et objets
Naissance, vie, mort
Toute valeur en mémoire suit le meme parcours, quel que soit le langage :
+-------------+ +-------------+ +-------------+
| Allocation | ---> | Utilisation | ---> | Liberation |
| (naissance) | | (lecture / | | (memoire |
| | | ecriture) | | recuperee) |
+-------------+ +-------------+ +-------------+
En C, tu contrôles les trois phases a la main. En Rust, le compilateur gere la libération via l'ownership. En JavaScript, tu contrôles les deux premières phases, et le garbage collector gere la troisieme.
C'est cette troisieme phase qui pose problème. Parce que tu ne decides pas quand elle arrive. Et parfois, elle n'arrive jamais.
Phase 1 : Allocation
L'allocation, c'est le moment ou la mémoire est reservee pour stocker une valeur. En JavaScript, ca se fait de manière implicite la plupart du temps. Tu ne vois pas le malloc.
typescript// Allocation implicite - tu ne vois rien de special
const n = 42; // un number sur la stack
const name = "Alice"; // une string (heap pour les longues)
const user = { name: "Alice" }; // un objet sur le heap
const scores = [1, 2, 3]; // un tableau sur le heap
const greet = () => "hello"; // une fonction sur le heap
// Allocation via appel de fonction
const now = new Date(); // un objet Date sur le heap
const copy = name.slice(0, 3); // une nouvelle string "Ali"
const doubled = scores.map(x => x * 2); // un nouveau tableau [2, 4, 6]
Chaque new, chaque litterale objet {}, chaque litterale tableau [], chaque appel de fonction qui retourne un objet, chaque méthode qui créé une copie... c'est une allocation.
Ce qui est traitre, c'est que certaines allocations sont cachees :
typescript// Ca a l'air innocent, mais ca alloue
const result = `Bonjour ${name}`; // nouvelle string (template literal)
const { age, ...rest } = user; // nouvel objet "rest"
const merged = { ...obj1, ...obj2 }; // nouvel objet
Phase 2 : Utilisation
L'utilisation, c'est lire ou écrire dans la mémoire allouee. C'est la phase la plus simple a comprendre : c'est ton code qui tourne.
typescript// Lecture
console.log(user.name);
const first = scores[0];
// Ecriture
user.name = "Bob";
scores.push(4);
Rien de magique ici. La seule subtilite, c'est que lire un objet via une référencé ne copie pas l'objet. Tu accedes a la meme zone mémoire sur le heap.
Phase 3 : Liberation
C'est la que ca devient interessant. En C, tu appelles free() toi-meme :
c// C - liberation manuelle
char* buffer = malloc(1024);
// ... utilisation ...
free(buffer); // tu decides quand liberer
En Rust, le compilateur inséré la libération automatiquement quand le proprietaire sort du scope :
rust// Rust - liberation automatique via ownership
{
let buffer = vec![0u8; 1024];
// ... utilisation ...
} // buffer est libere ici, automatiquement, deterministe
En JavaScript, tu ne fais rien. Tu esperes que le garbage collector va s'en occuper :
typescript// JavaScript - liberation par le GC (quand il veut)
function process() {
const buffer = new Uint8Array(1024);
// ... utilisation ...
} // buffer n'est plus accessible apres le return
// le GC le liberera... un jour
Le "un jour", c'est le problème. Le GC ne libéré pas immédiatement. Il attend d'avoir une bonne raison (besoin de mémoire, temps mort du moteur). Et surtout, il ne libéré que ce qui est inaccessible.
Qu'est-ce qu'un "garbage" ?
En ramasse-miettes, un "garbage" (dechet) est un objet qui n'est plus accessible depuis aucune racine (root). Les racines en JavaScript sont :
- Les variables globales (
window,global,globalThis) - La pile d'appels en cours (les variables locales des fonctions actives)
- Les callbacks en attente (timers, listeners, promises)
Si un objet est atteignable depuis une racine, directement ou via une chaîne de références, il est vivant. Sinon, il est garbage.
typescriptfunction example() {
const data = { value: 42 }; // data est accessible (variable locale)
return data.value; // on retourne la valeur primitive
}
// Apres le return, "data" n'est plus accessible
// L'objet { value: 42 } est du garbage
// Le GC peut le liberer
L'accessibilité : une chaîne de références
Le GC part des racines et suit toutes les références, comme un parcours de graphe :
RACINES (roots)
|
+-- variable globale "app"
| |
| +-- app.config -------> { theme: "dark" } [VIVANT]
| |
| +-- app.cache --------> Map { [VIVANT]
| "user1" --> { name: "Alice" } [VIVANT]
| }
|
+-- call stack
|
+-- variable locale "temp" --> { x: 1 } [VIVANT]
Objets sans reference depuis les racines :
{ old: "data" } [GARBAGE - sera libere]
{ stale: "cache" } [GARBAGE - sera libere]
Le piège classique, c'est l'objet qui te semble inutile mais qui est encore référencé quelque part :
typescriptlet cache: Map<string, object> = new Map();
function fetchData(key: string) {
const data = expensiveComputation(key);
cache.set(key, data); // reference dans le Map global
return data;
}
// Meme si tu n'utilises plus "data" apres l'avoir affiche,
// l'objet reste dans le Map.
// Le GC ne peut pas le liberer.
// C'est une fuite memoire.
Le contraste avec C et Rust
Pour mieux comprendre, voici comment les trois langages gerent la meme situation :
| Phase | C | Rust | JavaScript |
|-------------|---------------|-------------------|-------------------|
| Allocation | malloc() | Vec::new() | new Array() |
| Utilisation | *ptr = value | vec.push(value) | arr.push(value) |
| Liberation | free(ptr) | automatique (drop) | GC (non determin.)|
| Risque | double free, | aucun (compile- | fuites (reference |
| | use after free| time checks) | oubliee) |
Chaque approche a ses compromis. Le GC de JavaScript te libéré de la gestion manuelle, mais il te rend responsable de ne pas garder de références inutiles.
En pratique
Sur paltemps.fr, j'avais un pattern ou chaque requête API creait un objet de réponse, le transformait, le mettait en cache, et retournait un sous-ensemble au client. Trois allocations pour une seule requête. En comprenant le cycle de vie, j'ai pu réduire a une seule allocation en transformant les donnees en place plutot que de créer des copies intermediaires.
Résumé
- Toute valeur passe par trois phases : allocation, utilisation, libération.
- En JS, l'allocation est implicite (litterales,
new, appels de fonctions). - La libération est geree par le garbage collector, de manière non déterministe.
- Un objet est "garbage" uniquement quand il n'est plus accessible depuis aucune racine.
- Si tu gardes une référencé (meme accidentelle), le GC ne libéré rien.
Precedent : Stack vs Heap Suivant : Le garbage collector