W3docs

Ramasse-miettes Java (Garbage Collection)

Comment fonctionne le GC Java : accessibilité, tas générationnel, mark-sweep-compact, types de références et collecteurs.

En Java, vous n'appelez jamais free(). La JVM surveille chaque objet que vous allouez sur le tas et, lorsqu'un objet n'est plus accessible depuis votre programme en cours d'exécution, le ramasse-miettes (GC) récupère automatiquement sa mémoire. Vous écrivez du code qui crée des objets ; le GC nettoie discrètement derrière vous. Comprendre comment il décide ce qui est du déchet — et où il le cherche dans le tas — fait la différence entre un code qui monte en charge et un code qui se bloque sous la pression.

Cette page explique comment le GC décide ce qu'il faut conserver (accessibilité), comment le tas est organisé pour une collecte rapide, l'algorithme mark-sweep-compact, les quatre niveaux de référence, comment choisir un collecteur, et une démonstration exécutable qui rend la collecte observable.

Accessibilité et racines GC

Le GC ne cherche pas les objets dont vous avez « fini de vous servir ». Il cherche les objets qui sont encore accessibles. En partant d'un ensemble de racines GC, il suit chaque référence. Tout ce qu'il peut atteindre est vivant ; tout le reste est du déchet, que vous pensiez encore en avoir besoin ou non.

Racine GCExemple
Variables localesUne référence sur la pile d'un thread en cours d'exécution
Champs statiquesstatic final Logger LOG = ...
Threads actifsUn objet Thread vivant
Références JNIObjets détenus par du code natif

Affecter null à une référence (ou la laisser sortir de la portée) ne supprime rien — cela retire simplement un chemin vers l'objet. L'objet ne devient collectable que lorsqu'il n'existe plus aucun chemin depuis une racine.

Object a = new Object();   // reachable via local variable 'a'
Object b = a;              // now two references point to the same object
a = null;                  // still reachable through 'b' — not garbage
b = null;                  // now unreachable — eligible for collection

Le tas générationnel

La plupart des objets meurent jeunes — une portée de requête, une variable temporaire de boucle, une chaîne intermédiaire. La JVM exploite cette hypothèse générationnelle faible en divisant le tas en régions et en collectant la zone jeune beaucoup plus souvent que la vieille.

RégionContientCollectée
Jeune (Eden + 2 espaces Survivor)Objets fraîchement allouésFréquemment, par un minor GC rapide
Ancienne (Tenured)Objets ayant survécu à plusieurs minor GCRarement, par un major/full GC plus lent
MetaspaceMétadonnées de classes (pas vos objets)Lorsque les classloaders sont déchargés

Les nouveaux objets atterrissent dans Eden. Un minor GC copie les quelques survivants dans un espace Survivor ; les objets qui continuent de survivre sont finalement promus vers l'ancienne génération. Comme les minor GC ne balaient que la petite région jeune, ils sont bon marché — c'est pourquoi l'allocation de courte durée en Java est rapide. (Pour connaître les différences entre la pile et le tas, voir Stack vs Heap ; pour le rôle de la JVM qui héberge le tas, voir Architecture JVM.)

Marquer, balayer, compacter

Une collecte s'exécute en phases. D'abord elle marque chaque objet accessible en parcourant le graphe depuis les racines. Ensuite elle balaie, libérant les objets non marqués. De nombreux collecteurs ajoutent une phase de compactage qui regroupe les objets survivants afin que l'espace libre soit un bloc contigu — ce qui maintient l'allocation à un simple déplacement de pointeur et évite la fragmentation.

// Pseudocode of what the collector does for you:
// 1. mark:    visit(roots); for each reachable object, set live = true
// 2. sweep:   for each object on the heap, if !live -> reclaim its memory
// 3. compact: move survivors next to each other, update references

Vous pouvez suggérer une collecte avec System.gc(), mais ce n'est qu'un indice — la JVM peut l'ignorer. Ne vous y fiez jamais pour la correction du programme ; traitez-le comme un outil de diagnostic, pas comme une stratégie de gestion de la mémoire.

Niveaux de référence

Toutes les références ne maintiennent pas un objet en vie de la même façon. Le package java.lang.ref vous permet d'indiquer au GC à quel point vous souhaitez qu'un objet soit conservé, ce qui constitue la base des caches sensibles à la mémoire.

RéférenceComportement du GC
Forte (affectation ordinaire =)Jamais collectée tant qu'elle est accessible
SoftReferenceCollectée uniquement lorsque la mémoire est basse — idéale pour les caches
WeakReferenceCollectée au prochain GC dès qu'il n'y a plus de références fortes
PhantomReferenceUtilisée pour planifier un nettoyage après la collecte
import java.lang.ref.WeakReference;

byte[] data = new byte[1024];
WeakReference<byte[]> ref = new WeakReference<>(data);
data = null;            // drop the only strong reference
// After the next GC, ref.get() may return null.

Les fuites mémoire se produisent quand même

Un ramasse-miettes vous libère des pointeurs pendants et des doubles libérations, mais pas des fuites. Une fuite mémoire Java est un objet que vous n'utilisez plus mais qui est encore accessible depuis une racine, de sorte que le GC doit le conserver. Le tas se remplit, le GC tourne de plus en plus souvent, et vous finissez par obtenir une OutOfMemoryError.

Les causes classiques sont toutes liées au fait d'avoir « oublié de lâcher » :

  • Une collection static (cache, liste d'écouteurs, map) à laquelle vous continuez d'ajouter des éléments sans jamais en supprimer. Les champs statiques sont des racines GC, donc tout ce qu'ils atteignent vit indéfiniment.
  • Des écouteurs ou des callbacks enregistrés auprès d'un objet à longue durée de vie et jamais désinscrits.
  • Des clés laissées dans une HashMap longtemps après qu'elles ne sont plus nécessaires, car la map les référence encore.

La solution n'est pas un flag — c'est de supprimer les références lorsque vous avez terminé (retirer de la collection, désinscrire l'écouteur) ou d'utiliser une structure basée sur WeakReference telle que WeakHashMap pour que le GC puisse récupérer les entrées dès que rien d'autre ne pointe vers la clé.

Avertissement
Java n'a pas de destructeurs, et finalize() est déprécié et peu fiable — la JVM peut l'exécuter tardivement ou pas du tout. Pour libérer des fichiers, des sockets ou d'autres ressources non-mémoire de façon déterministe, utilisez try-with-resources et AutoCloseable, et non le ramasse-miettes.

Choisir un collecteur

La JVM HotSpot est livrée avec plusieurs collecteurs offrant différents compromis entre débit (travail total effectué) et latence (durée des pauses). Vous en choisissez un avec un flag JVM ; le collecteur par défaut depuis Java 9 est G1.

CollecteurFlagIdéal pour
G1 (par défaut)-XX:+UseG1GCLatence/débit équilibrés, grands tas
Parallel-XX:+UseParallelGCTraitements batch privilégiant le débit brut
ZGC-XX:+UseZGCTrès grands tas, pauses sous la milliseconde
Serial-XX:+UseSerialGCPetits tas, cœur unique ou conteneurs
# Pick a collector and set the heap size at launch:
java -XX:+UseG1GC -Xms256m -Xmx2g MyApp

# Print what the GC is doing, with timestamps:
java -Xlog:gc* MyApp

Exemple concret

Le programme ci-dessous rend le comportement du GC observable. Il maintient un objet avec une référence forte, en détient un autre uniquement via une WeakReference, génère une vague de déchets à courte durée de vie, puis demande une collecte et rapporte ce qui a survécu et comment l'utilisation du tas a évolué.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Le référent faible affiche true avant la collecte et false après, prouvant qu'une WeakReference ne maintient pas son objet en vie dès qu'il n'y a plus de référence forte.
  • Le tableau kept fortement référencé affiche survived: true même après System.gc(), car il est accessible depuis une racine GC et le collecteur doit le préserver.
  • Environ 300 Mo de déchets sont alloués (Bytes allocated as garbage: 307200000), mais le tas utilisé ne monte qu'à environ 5 Mo — les minor GC récupèrent les tableaux de boucle à courte durée de vie aussi vite qu'ils sont créés.
  • Runtime.maxMemory() indique le plafond du tas (environ 256 Mo ici), défini par -Xmx, tandis que totalMemory() - freeMemory() est la portion vivante utilisée qui oscille entre 3 et 5 Mo tout au long.
  • System.gc() n'est qu'un indice, mais sur cette JVM il s'exécute bien : le tas utilisé redescend et le référent faible inaccessible est effacé plutôt que de persister.

Pratique

Pratique
Un objet est référencé uniquement par une variable locale qui vient de sortir de sa portée, et par rien d'autre. Qu'est-ce qui le rend éligible au ramasse-miettes ?
Un objet est référencé uniquement par une variable locale qui vient de sortir de sa portée, et par rien d'autre. Qu'est-ce qui le rend éligible au ramasse-miettes ?
Was this page helpful?