01 - Stack vs Heap : ou vivent tes variables
Ce que tu vas apprendre
- Ce qu'est la stack (pile d'appels) et comment elle fonctionne
- Ce qu'est le heap (tas) et pourquoi les objets y vivent
- Pourquoi les primitives sont sur la stack et les objets sur le heap
- Ce qui se passe quand la stack deborde
Prerequisites
- Avoir lu l'introduction de la serie
- Connaitre les bases de JavaScript (variables, fonctions, objets)
La metaphore du bureau
Imagine ton bureau. Tu as une pile de post-its a droite (la stack) et un grand tiroir a gauche (le heap). Quand tu appelles une fonction, tu poses un post-it sur la pile avec les infos locales : les arguments, les variables simples. Quand la fonction se termine, tu retires le post-it. Simple, rapide, automatique.
Mais quand tu créés un objet, un tableau, une fonction, ca ne tient pas sur un post-it. Tu le ranges dans le tiroir (le heap), et tu notes sur ton post-it l'adresse du tiroir ou le trouver. C'est ca, une référencé.
La stack : rapide, ordonnee, limitee
La stack est une structure LIFO (Last In, First Out). Chaque appel de fonction créé un call frame (cadre d'appel) qui contient :
- Les arguments de la fonction
- Les variables locales (primitives :
number,string,boolean,"null","undefined",bigint,symbol) - L'adresse de retour (ou reprendre apres la fonction)
typescriptfunction add(a: number, b: number): number {
const result = a + b;
return result;
}
function main() {
const x = 10;
const y = 20;
const sum = add(x, y);
console.log(sum);
}
main();
Voici ce qui se passe dans la stack pendant l'exécution :
Etape 1 : main() est appelee
+-----------------+
| main |
| x = 10 |
| y = 20 |
+-----------------+
Etape 2 : add(10, 20) est appelee
+-----------------+
| add |
| a = 10 |
| b = 20 |
| result = 30 |
+-----------------+
| main |
| x = 10 |
| y = 20 |
+-----------------+
Etape 3 : add() retourne 30, son frame est retire
+-----------------+
| main |
| x = 10 |
| y = 20 |
| sum = 30 |
+-----------------+
La stack est rapide parce que l'allocation et la desallocation sont triviales : on deplace un pointeur. Pas besoin de chercher un espace libre. Pas besoin de garbage collector.
Mais la stack est limitee en taille. Sur V8 (Node.js/Chrome), c'est typiquement quelques Mo. Suffisant pour des milliers d'appels imbriques, mais pas pour des millions.
Le heap : flexible, coûteux, gere par le GC
Le heap est une zone de mémoire beaucoup plus grande (des centaines de Mo a plusieurs Go). C'est la que vivent tous les objets, tableaux, fonctions, closures, et tout ce qui a une taille dynamique.
typescriptfunction createUser(name: string, age: number) {
// Cet objet est alloue sur le heap
const user = {
name, // la string est aussi sur le heap
age, // le number est copie dans l'objet sur le heap
scores: [], // le tableau vide est sur le heap
};
return user;
}
const alice = createUser("Alice", 30);
Quand tu ecris const alice = createUser(...), la variable alice est sur la stack. Mais sa valeur est une référencé (une adresse) qui pointe vers l'objet sur le heap.
STACK HEAP
+---------------+ +-------------------+
| alice = 0xA3 | ---------> | { name: "Alice", |
+---------------+ | age: 30, |
| scores: [] |
+-------------------+
Primitives vs objets : la regle
La regle est simple :
- Primitives (
number,string,boolean,"null","undefined",bigint,symbol) : stockees directement sur la stack (dans le call frame) - Objets (objets, tableaux, fonctions,
Map,Set, etc.) : alloues sur le heap, référencé sur la stack
Ca explique un comportement que tu connais deja :
typescript// Primitives : copie de valeur
let a = 42;
let b = a;
b = 100;
console.log(a); // 42 - pas affecte
// Objets : copie de reference
let obj1 = { value: 42 };
let obj2 = obj1;
obj2.value = 100;
console.log(obj1.value); // 100 - affecte !
Primitives (copie de valeur) :
STACK
+-------+ +-------+
| a = 42| | b = 100| (deux valeurs independantes)
+-------+ +-------+
Objets (copie de reference) :
STACK HEAP
+-------------+ +--------------+
| obj1 = 0xB7 | ------>| { value: 100 }|
+-------------+ / +--------------+
| obj2 = 0xB7 | --/
+-------------+
(deux references vers le meme objet)
Stack overflow : quand la pile deborde
La stack a une taille fixe. Si tu appelles trop de fonctions sans que les précédentes se terminent, tu depasses la limite :
typescriptfunction infinite(): void {
infinite(); // appel recursif sans condition d'arret
}
infinite();
// RangeError: Maximum call stack size exceeded
Chaque appel a infinite() ajoute un frame sur la stack. Apres ~10 000 a ~15 000 appels (ca depend du moteur et de la taille des frames), la stack est pleine. C'est le fameux stack overflow.
C'est aussi pour ca que les algorithmes recursifs profonds posent problème en JavaScript. Pas de tail call optimization fiable (seul Safari l'implemente). Si tu as une recursion de 100 000 niveaux, il faut la transformer en boucle.
Le cas special des strings
Les strings en JavaScript sont des primitives... mais pas vraiment stockees sur la stack quand elles sont longues. V8 les alloue sur le heap et met la référencé sur la stack. Les petites strings (quelques caractères) peuvent etre internalisees (string interning) et partagees.
typescriptconst a = "hello"; // probablement internalisee
const b = "hello"; // meme reference interne que a
const c = "a".repeat(10_000); // forcement sur le heap
C'est un détail d'implementation de V8, mais ca explique pourquoi la concatenation massive de strings peut consommer beaucoup de mémoire. On en reparlera dans l'article sur les strings.
En pratique sur paltemps.fr
Sur paltemps.fr, les donnees meteo arrivent sous forme de gros tableaux JSON (previsions heure par heure sur 7 jours). Chaque tableau est un objet sur le heap. Si je gardais toutes les réponses API en mémoire pour faire du cache, le heap explosait en quelques heures. Comprendre que ces objets vivent sur le heap et que le GC ne les libéré que quand plus rien ne les référencé, c'est ce qui m'a amene a utiliser un cache avec TTL.
Résumé
- La stack est rapide, ordonnee (LIFO), et contient les primitives et les références. Elle a une taille fixe.
- Le heap est grand, flexible, et contient tous les objets. Il est gere par le garbage collector.
- Les primitives sont copiees par valeur. Les objets sont copies par référencé.
- Un stack overflow arrive quand la pile d'appels dépassé sa taille limite (recursion infinie ou trop profonde).
Precedent : Introduction - pourquoi la mémoire compte Suivant : Cycle de vie de la mémoire