Mémoire et performance JS/TS - 02 - Le cycle de vie de la mémoire

Allocation, utilisation, libération : les trois phases de la mémoire en JavaScript et ce que le garbage collector fait vraiment.

  1. 01 Mémoire et performance JS/TS - 00 - Pourquoi la mémoire compte meme avec un garbage collector
  2. 02 Mémoire et performance JS/TS - 01 - Stack vs Heap
  3. 03 Mémoire et performance JS/TS - 02 - Le cycle de vie de la mémoire
  4. 04 Mémoire et performance JS/TS - 03 - Le garbage collector
  5. 05 Mémoire et performance JS/TS - 04 - V8 en profondeur
  6. 06 Mémoire et performance JS/TS - 05 - Les 6 fuites mémoire classiques
  7. 07 Mémoire et performance JS/TS - 06 - Closures et mémoire
  8. 08 Mémoire et performance JS/TS - 07 - WeakRef, WeakMap et WeakSet
  9. 09 Mémoire et performance JS/TS - 08 - FinalizationRegistry : savoir quand le GC passe
  10. 10 Mémoire et performance JS/TS - 09 - DevTools Memory : investiguer dans Chrome
  11. 11 Mémoire et performance JS/TS - 10 - Profiling mémoire en Node.js
  12. 12 Mémoire et performance JS/TS - 11 - Détecter et corriger les fuites mémoire
  13. 13 Mémoire et performance JS/TS - 12 - ArrayBuffer et TypedArrays
  14. 14 Mémoire et performance JS/TS - 13 - Workers et mémoire partagee
  15. 15 Mémoire et performance JS/TS - 14 - Streams et backpressure
  16. 16 Mémoire et performance JS/TS - 15 - Fuites mémoire en React
  17. 17 Mémoire et performance JS/TS - 16 - Serveurs Node.js et mémoire
  18. 18 Mémoire et performance JS/TS - 17 - Mémoire et Docker
  19. 19 Mémoire et performance JS/TS - 18 - Optimisations mémoire
  20. 20 Mémoire et performance JS/TS - 19 - Comparaison avec d'autres langages
  21. 21 Mémoire et performance JS/TS - 20 - Tester la mémoire
  22. 22 Mémoire et performance JS/TS - 21 - Glossaire

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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.