1. Vue d’ensemble de la mémoire d’un processus Java
Lorsque vous lancez un programme Java, la JVM (Java Virtual Machine) demande au système d’exploitation un morceau de mémoire. Parfois modeste, parfois très conséquent (surtout si vous lancez un Minecraft rempli de mods). Cette mémoire est divisée en plusieurs zones clés, chacune jouant son propre rôle :
- Pile (Stack) — pour les variables locales et les appels de méthodes.
- Tas (Heap) — pour tous les objets que vous créez avec new.
- Zones système (PermGen/MetaSpace) — pour les métadonnées de classes, les champs statiques et autres choses « magiques ».
Cela ressemble à peu près à ceci :
┌───────────────────────────────┐
│ Processus JVM │
│ ┌─────────────┐ │
│ │ Stack │ ← Chaque thread — sa propre pile !
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Commun à tous les threads
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ PermGen/ │ ← Métadonnées des classes
│ │ MetaSpace │
│ └───────────────┘ │
└───────────────────────────────┘
Pourquoi est-ce important ?
- Comprendre l’organisation de la mémoire aide à écrire un code plus efficace et plus sûr.
- Il est plus simple de diagnostiquer des erreurs comme StackOverflowError ou OutOfMemoryError.
- Les mots « ramasse-miettes » et « fuite de mémoire » ne font plus peur — vous savez où et quoi chercher.
2. Pile (Stack) : rapide, locale, mais pas éternelle
La pile est une zone spéciale de la mémoire, allouée pour chaque thread séparément. La pile ressemble à une pile d’assiettes : la dernière posée — la première retirée, mais seulement après avoir retiré toutes celles au-dessus. Autrement dit, la pile fonctionne selon le principe LIFO (Last In, First Out).
À quoi sert la pile ?
Dans la pile, on stocke :
- Les variables locales des méthodes (par exemple, int x = 5; à l’intérieur d’une méthode).
- L’adresse de retour après l’appel d’une méthode (pour savoir où revenir après l’exécution de la méthode).
Chaque fois que vous appelez une méthode, un nouveau frame (stack frame) est ajouté à la pile — une sorte de boîte qui contient toutes les variables locales de cette méthode et des informations de service. Quand la méthode se termine, son frame est supprimé — toutes ses variables locales disparaissent.
Exemple
public static void main(String[] args) {
int a = 10; // a est sur la pile de main
int b = sum(a, 5); // on appelle sum
}
public static int sum(int x, int y) {
int result = x + y; // x, y, result sont sur la pile de sum
return result;
}
- Quand sum est appelée, un frame distinct est créé dans la pile pour elle.
- Après la fin de sum, ses variables disparaissent.
Cycle de vie d’une variable
Les variables locales vivent uniquement tant que la méthode dans laquelle elles sont déclarées s’exécute. Dès que la méthode se termine — elles n’existent plus, la mémoire est libérée instantanément.
Débordement de pile
Si vous avez par inadvertance (ou volontairement) écrit une récursion infinie, chaque appel de méthode ajoutera un nouveau frame à la pile. À un moment donné, la pile sera pleine et vous obtiendrez :
Exception in thread "main" java.lang.StackOverflowError
Exemple :
public static void main(String[] args) {
recurse();
}
public static void recurse() {
recurse(); // Récursion infinie !
}
Taille de la pile
La taille de la pile est limitée — en général, quelques mégaoctets par thread (elle peut être définie avec le paramètre -Xss). Si la pile est épuisée — le programme se termine par une erreur.
3. Tas (Heap) : l’endroit pour vos objets
Le tas (Heap) est une zone de mémoire partagée par tous les threads, où vivent tous les objets que vous créez à l’aide de new, ainsi que les tableaux. C’est dans le tas que se produit toute la magie de la programmation orientée objet.
Comment les objets vont-ils dans le tas ?
String s = new String("Hello");
int[] arr = new int[10];
- La variable s est une référence ; elle se trouve sur la pile.
- L’objet String lui‑même et le tableau arr — se trouvent dans le tas.
Cycle de vie d’un objet
Un objet vit dans le tas tant qu’il existe au moins une référence forte (strong reference) vers lui. Dès que plus personne ne fait référence à l’objet — il devient un « déchet » et peut être supprimé par le collecteur (GC).
Gestion de la mémoire
Contrairement à C/C++, où vous devez vous‑même libérer la mémoire (free, delete), en Java c’est le GC qui s’en charge. Vous ne pouvez pas libérer explicitement un objet, mais vous pouvez annuler toutes les références vers lui — il deviendra alors candidat à la suppression.
Schéma : où se trouve quoi ?
Stack (main)
└─ s ─┬────────────┐
│ │
▼ │
Heap │
┌─────────────┐ │
│ String "Hello"◄──┘
└─────────────┘
Particularités du tas
- Le tas est unique pour tout le processus JVM.
- La taille du tas peut être définie au lancement (-Xmx, -Xms).
- Si le tas n’a plus d’espace libre et que le GC ne peut pas en libérer — le programme se termine par l’erreur OutOfMemoryError.
4. PermGen et MetaSpace : où vivent les classes ?
Quand vous écrivez class MyClass { ... } puis lancez le programme, la JVM doit stocker quelque part tout ce qui est lié à cette classe — méthodes, champs, bytecode, variables statiques, constantes et même les littéraux de chaîne. Pour cela, la JVM dispose d’une zone de mémoire spéciale où « vivent » les classes.
Avant Java 8, cette zone s’appelait PermGen (Permanent Generation). Mais elle avait de nombreux problèmes — par exemple, une taille fixe ; si l’espace venait à manquer, l’application se terminait tout simplement par OutOfMemoryError: PermGen space.
Avec Java 8, une zone nouvelle et plus flexible est apparue — MetaSpace. Elle a remplacé l’ancienne PermGen et peut désormais s’agrandir automatiquement, en utilisant autant de mémoire que nécessaire (dans la limite de la mémoire physique disponible).
PermGen (avant Java 8)
- PermGen contenait les métadonnées des classes, les champs statiques, les littéraux de chaîne.
- La taille de PermGen était limitée (par défaut assez petite), on pouvait l’augmenter avec le paramètre -XX:MaxPermSize=256m.
- Si de nombreuses classes étaient chargées dynamiquement par l’application (par exemple, sur des serveurs web), PermGen pouvait « se remplir », et vous obteniez l’erreur :
java.lang.OutOfMemoryError: PermGen space
- Problème : le nettoyage de PermGen ne se produisait pas toujours correctement si les classes étaient déchargées dynamiquement (par exemple, lors du rechargement d’applications web).
MetaSpace (Java 8+)
- Depuis Java 8, PermGen a disparu et MetaSpace est apparue.
- MetaSpace stocke les métadonnées de classes, mais désormais dans la mémoire native (en dehors du tas Java).
- La taille de MetaSpace n’est pas limitée par défaut (limitée uniquement par la mémoire du système), mais on peut fixer une limite via -XX:MaxMetaspaceSize=512m.
- L’erreur en cas de manque de mémoire ressemble désormais à ceci :
java.lang.OutOfMemoryError: Metaspace
- Dans MetaSpace se trouvent également les champs statiques, les méthodes et les informations sur les classes.
Schéma : comment tout est organisé
┌───────────────────────────────┐
│ Processus JVM │
│ ┌─────────────┐ │
│ │ Stack │ ← Variables locales, appels de méthodes
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Objets, tableaux, tout ce qui est créé avec new
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ MetaSpace │ ← Métadonnées des classes, champs statiques
│ └───────────────┘ │
└───────────────────────────────┘
Pourquoi est-ce important ?
Si vous développez des applications desktop ou serveur classiques, vous ne rencontrerez probablement jamais d’erreurs PermGen ou MetaSpace. Mais si vous travaillez avec le chargement dynamique de classes (par exemple, des plugins, des applications web, des frameworks comme Spring qui peuvent charger et décharger beaucoup de classes), alors connaître MetaSpace est indispensable !
5. Illustration : schéma de la mémoire de la JVM
flowchart TD
subgraph JVM
direction TB
Stack1["Pile (Thread 1)"]
Stack2["Pile (Thread 2)"]
Heap[Heap]
MetaSpace[MetaSpace]
end
Stack1 --réfère à--> Heap
Stack2 --réfère à--> Heap
Heap --utilise des classes de--> MetaSpace
- Chaque thread a sa propre pile.
- Toutes les piles peuvent référencer des objets dans le tas.
- Les objets du tas « savent » leur classe, dont l’information se trouve dans MetaSpace.
6. Exemple : à quoi cela ressemble dans du code réel
public class MemoryDemo {
public static void main(String[] args) {
int x = 42; // x se trouve sur la pile de main
String s = "Hello!"; // s — une référence sur la pile, l’objet String dans le tas, le littéral "Hello!" dans MetaSpace
Person p = new Person("Alice"); // p — une référence sur la pile, l’objet Person dans le tas
// Appelons une méthode pour créer un nouveau frame de pile
printPerson(p);
}
public static void printPerson(Person person) {
// person — une référence sur la pile de printPerson
System.out.println(person.getName());
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name; }
}
Analyse :
- x — variable locale, vit sur la pile de la méthode main.
- s — référence sur la pile, l’objet String dans le tas, et le littéral de chaîne "Hello!" — dans MetaSpace.
- p — référence sur la pile, l’objet Person dans le tas.
- La classe Person et toutes ses méthodes/champs — dans MetaSpace (métadonnées des classes).
- L’appel printPerson(p) crée un nouveau frame de pile ; à l’intérieur, la référence locale person pointe vers le même objet dans le tas.
7. Comment la JVM gère la mémoire : FAQ rapide
Puis-je gérer la pile ?
Non, la pile est entièrement sous le contrôle de la JVM. Vous pouvez seulement en définir la taille au lancement (-Xss).
Puis-je gérer le tas ?
Partiellement : la taille du tas est définie au lancement (-Xmx, -Xms). Le nettoyage est assuré par le ramasse-miettes (GC).
Puis-je gérer MetaSpace ?
Vous pouvez en limiter la taille (-XX:MaxMetaspaceSize), mais en général ce n’est pas nécessaire.
Que se passe-t-il en cas de manque de mémoire ?
— Si la pile est épuisée — StackOverflowError.
— Si le tas est épuisé — OutOfMemoryError: Java heap space.
— Si MetaSpace est épuisée — OutOfMemoryError: Metaspace.
8. Erreurs courantes liées à la mémoire
Erreur n°1 : StackOverflowError due à une récursion infinie. La raison la plus fréquente — vous avez oublié de prévoir une condition d’arrêt à la récursion. Par exemple, une méthode s’appelle elle‑même sans s’arrêter. La JVM ne peut pas étendre la pile à l’infini, et le programme « tombera ».
Erreur n°2 : OutOfMemoryError due au débordement du tas. Si vous créez trop d’objets vers lesquels continuent de pointer des variables/collections (par exemple, vous ajoutez des éléments dans une liste sans jamais les supprimer), le tas peut se remplir.
Erreur n°3 : OutOfMemoryError: PermGen space / Metaspace. Si vous utilisez des plugins ou chargez dynamiquement beaucoup de classes, et que MetaSpace ne se nettoie pas (par exemple, à cause d’un déchargement incorrect des classes), l’espace de MetaSpace peut s’épuiser.
Erreur n°4 : Confusion entre référence et objet. Beaucoup de débutants confondent : une variable de type Person dans la pile — ce n’est qu’une référence, tandis que l’objet lui‑même — est dans le tas.
Erreur n°5 : S’attendre à ce que le ramasse-miettes libère tout instantanément. Le GC fonctionne « selon son humeur » (en réalité — selon ses algorithmes internes et en cas de manque de mémoire), et non pas immédiatement après que vous avez annulé une référence. Il ne faut pas compter sur une libération immédiate de la mémoire.
GO TO FULL VERSION