W3docs

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égionCe qui y vitFréquence de collecte
EdenObjets 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, promusGC majeur / complet
MetaspaceMé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 compaction

Le tableau ci-dessous est le modèle mental à garder en tête :

CollecteurForceComportement des pausesUtilisation typique
SerialPlus simple, faible empreinteStop-the-world, thread uniquePetits tas, conteneurs, CLIs
ParallelDébit le plus élevéStop-the-world, plusieurs threadsTraitement par lots / de données
G1Équilibré, prévisibleMajoritairement concurrent, cible de pausePar défaut usage général
ZGCTrès faible latenceSous la milliseconde, concurrentTas multi-Go à To
ShenandoahTrès faible latencePauses indépendantes de la taille du tasServices 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.832ms

Cette 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 false

L'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.

java— editable, runs on the server

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

  • L'étape 2 affiche true : l'objet observé est encore fortement accessible via la variable garbage, donc la WeakReference peut 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 que garbage = null l'a rendu inaccessible et que System.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 : le survivor, 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.

Pratique

Pratique
Quelle est la propriété unique qui détermine si le ramasse-miettes conservera un objet, quel que soit l'algorithme utilisé ?
Quelle est la propriété unique qui détermine si le ramasse-miettes conservera un objet, quel que soit l'algorithme utilisé ?
Was this page helpful?