Algorithmes GC de Java
Comparez les principaux ramasse-miettes Java — Serial, Parallel, G1, ZGC et Shenandoah — et leurs compromis.
La JVM vous libère de la désallocation manuelle de la mémoire : un ramasse-miettes (GC) s'exécute en arrière-plan, recherche les objets que votre programme ne peut plus atteindre et libère leur espace. Mais « le ramasse-miettes » n'est pas une chose unique. La JVM HotSpot embarque plusieurs collecteurs, chacun faisant un compromis différent entre le débit (quelle part du CPU va à votre application plutôt qu'au GC), la latence (combien de temps l'application se met en pause pendant que le GC travaille) et l'empreinte mémoire (combien de mémoire le collecteur lui-même consomme).
Bien choisir est important. Un traitement par lots qui calcule des données toute la nuit veut un débit maximal et se moque des pauses ; un système de trading ou un serveur web veut les pauses les plus courtes possibles même si le débit total baisse un peu. Ce chapitre compare les collecteurs en production et montre ce qu'ils ont tous en commun : un objet vit exactement aussi longtemps qu'il est accessible.
Comment les collecteurs décident ce qu'ils conservent
Chaque collecteur HotSpot répond à la même question — quels objets sont encore utilisés — de la même façon : il trace l'accessibilité depuis un ensemble de racines GC (variables locales sur la pile, champs statiques, threads actifs). Tout objet accessible depuis une racine est vivant ; tout le reste est du garbage. La portée et l'âge ne sont pas pertinents ; seule l'accessibilité compte. Pour une vue d'ensemble de la façon dont cela s'intègre dans le runtime, voir Garbage Collection Java et Pile vs Tas.
public class Reachability {
public static void main(String[] args) {
String a = new String("kept"); // reachable via local variable 'a'
String b = new String("dropped"); // reachable via 'b'...
b = null; // ...until now: "dropped" is unreachable
System.out.println(a); // 'a' is still a GC root reference
}
}Au moment où b = null s'exécute, l'objet "dropped" n'a plus de chemin depuis aucune racine et devient éligible à la collecte. Le collecteur peut le récupérer immédiatement, bien plus tard, ou — si le programme se termine en premier — jamais. Vous n'appelez jamais free ; vous arrêtez simplement de référencer.
Disposition générationelle du tas
La plupart des objets Java meurent jeunes. Les collecteurs exploitent cela avec un tas générationnel : les nouveaux objets atterrissent dans la jeune génération (Eden plus deux espaces survivants), et les objets qui survivent à plusieurs collectes sont promus vers la vieille génération. Collecter fréquemment la petite jeune génération, riche en garbage, est bon marché ; la grande vieille génération est collectée bien moins souvent.
| Région | Ce qui y vit | Fréquence de collecte |
|---|---|---|
| Eden | Objets fraîchement alloués | À chaque GC mineur |
| Survivant (S0/S1) | Objets ayant survécu à un GC mineur | À chaque GC mineur |
| Vieille (tenured) | Objets de longue durée, promus | GC majeur / complet |
| Metaspace | Métadonnées de classe (hors tas) | Lors du déchargement de classe |
Un GC mineur nettoie la jeune génération et est rapide ; un GC majeur ou complet touche la vieille génération et est la source des longues pauses que les gens redoutent.
Comparaison des collecteurs
HotSpot vous permet de choisir un collecteur avec un seul flag, et chacun est optimisé pour un objectif différent. Vous changez rarement l'algorithme dans le code — vous le définissez en ligne de commande.
java -XX:+UseSerialGC MyApp # single-threaded, tiny heaps
java -XX:+UseParallelGC MyApp # throughput-oriented, multi-threaded
java -XX:+UseG1GC MyApp # balanced, the default since Java 9
java -XX:+UseZGC MyApp # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp # low pause, concurrent compactionLe tableau ci-dessous est le modèle mental à garder en tête :
| Collecteur | Force | Comportement des pauses | Utilisation typique |
|---|---|---|---|
| Serial | Plus simple, faible empreinte | Stop-the-world, thread unique | Petits tas, conteneurs, CLIs |
| Parallel | Débit le plus élevé | Stop-the-world, plusieurs threads | Traitement par lots / de données |
| G1 | Équilibré, prévisible | Majoritairement concurrent, cible de pause | Par défaut usage général |
| ZGC | Très faible latence | Sous la milliseconde, concurrent | Tas multi-Go à To |
| Shenandoah | Très faible latence | Pauses indépendantes de la taille du tas | Services réactifs |
G1 (« Garbage-First ») est le collecteur par défaut depuis Java 9. Il divise le tas en régions de taille égale et collecte en priorité les régions avec le plus de garbage, en visant une cible de temps de pause définie avec -XX:MaxGCPauseMillis=200.
Concurrent vs stop-the-world
L'axe crucial est le moment où les threads de l'application doivent s'arrêter. Les collecteurs stop-the-world (STW) (Serial, Parallel) mettent en pause tous les threads de l'application pendant leur travail — simples et à haut débit, mais la pause augmente avec le tas. Les collecteurs concurrents (ZGC, Shenandoah, et la majeure partie de G1) font l'essentiel de leur travail pendant que vos threads continuent de s'exécuter, de sorte que les pauses restent courtes même quand les tas atteignent des gigaoctets ou des téraoctets.
# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832msCette ligne de log mérite d'être décodée : 24M->5M(64M) signifie que le tas utilisé est passé de 24 Mo à 5 Mo sur un total de 64 Mo, et l'application s'est mise en pause pendant 1.832ms. Lire la sortie -Xlog:gc est la compétence d'optimisation GC la plus utile — mesurez avant de changer un flag.
Observer la collecte depuis le code
Vous ne pouvez pas invoquer directement un algorithme spécifique depuis Java, mais vous pouvez observer la collecte en cours. Une WeakReference vous permet de conserver un pointeur qui ne maintient pas sa cible en vie, afin de demander « cet objet a-t-il déjà été collecté ? » La classe Runtime rapporte l'utilisation du tas, et System.gc() est un indice — jamais une commande — pour exécuter une collecte maintenant.
import java.lang.ref.WeakReference;
WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC: " + (ref.get() != null)); // usually falseL'exemple exécutable ci-dessous assemble ces éléments : il alloue une vague de garbage, maintient un survivant accessible, observe un objet inaccessible via une référence faible, et mesure le tas avant et après une collecte.
Ce qu'il faut retenir de l'exécution :
- L'étape 2 affiche
true: l'objet observé est encore fortement accessible via la variablegarbage, donc laWeakReferencepeut le lire. - L'étape 3 rapporte le tas utilisé après 300 000 allocations de courte durée. Le nombre exact varie d'une exécution à l'autre — un GC mineur peut déjà avoir balayé une grande partie de cette vague en cours de boucle — mais la créer est précisément la sollicitation de la jeune génération que tout collecteur générationnel est conçu pour gérer à faible coût.
- L'étape 4 affiche
true, confirmant que l'objet observé a été récupéré une fois quegarbage = nulll'a rendu inaccessible et queSystem.gc()a déclenché une collecte — preuve que la perte d'accessibilité, et non la sortie de portée, est ce qui libère la mémoire. - L'étape 5 affiche
true: lesurvivor, encore référencé par une variable locale vivante (une racine GC), traverse la collecte intact. - L'étape 6 montre le tas utilisé revenir près de la valeur de base, démontrant que le collecteur restitue l'espace récupéré pour réutilisation plutôt que le programme ne le fuie.
Pour en savoir plus sur les types de références utilisés ici — forte, douce, faible et fantôme — voir Références Java.