Exploration des collecteurs de nouvelle génération : G1, ZGC et Shenandoah GC

L'évolution du Java Development Kit (JDK) a transformé la gestion de la mémoire, passant de modèles strictement générationnels à des architectures basées sur le partitionnement. Depuis le JDK 11, de nouveaux algorithmes comme ZGC et Shenandoah ont rejoint G1 pour offrir des performances accrues, notamment sur des volumes de données massifs.

1. G1 : Le pionnier de la récupération par régions

Introduit comme successeur de CMS, le Garbage-First (G1) a marqué un tournant en divisant le tas (heap) non plus en segments physiques fixes (Young/Old), mais en une multitude de petites Régions de taille identique. Bien que les concepts de génération (Eden, Survivor, Old) persistnet logiquement, leur emplacement physique est dynamique.

Structure de la mémoire et Régions

G1 divise le tas en environ 2048 régions. Chaque région peut changer de rôle dynamiquement. Une catégorie spécifique, appelée Humongous, est réservée aux objets occupant plus de 50 % de la taille d'une région standard, évitant ainsi leur copie répétée entre les générations classiques.

Structure des régions G1#### Cycles de collecte et mixité

G1 alterne entre plusieurs types de pauses :

  • Young GC : Déclenché lorsque l'espace Eden est saturé. G1 évalue le temps nécessaire pour nettoyer les régions jeunes par rapport à l'objectif de pause fixé par -XX:MaxGCPauseMillis.
  • Mixed GC : Se produit lorsque l'occupation du tas atteint un certain seuil. Il nettoie toutes les régions jeunes et une sélection de régions anciennes présentant le meilleur ratio "déchets/temps de traitement".

Algorithme de marquage et SATB

Pour gérer les modifications de références pendant le cycle concurrent, G1 utilise le mécanisme SATB (Snapshot-At-The-Beginning) associé à des barrières d'écriture. L'idée est de conserver une "image" des objets vivants au début du cycle.

// Simulation simplifiée d'une barrière d'écriture SATB
void barriere_ecriture_prealable(Object* champ) {
    if (collecte_en_cours && !est_marque(champ)) {
        Object ancienne_valeur = *champ;
        // On enregistre l'ancienne référence pour ne pas la perdre durant le marquage
        liste_marquage_remarque.ajouter(ancienne_valeur);
    }
}

void affectation_champ(Object* champ, Object nouvelle_val) {
    barriere_ecriture_prealable(champ); 
    *champ = nouvelle_val; 
}

Ce procédé garantit que les objets vivants au début du marquage ne sont pas supprimés par erreur, au prix d'une légère création de "déchets flottants".

2. ZGC : L'ambition de la latence ultra-faible

Disponible depuis le JDK 11, ZGC (Z Garbage Collector) est un collecteur évolutif conçu pour des tas allant de quelques Go à plusieurs To, avec une promesse de pauses inférieures à 10ms.

Architecture par pages et NUMA

ZGC utilise des pages (Small, Medium, Large) plutôt que des régions fixes. Il est nativement compatible avec l'architecture NUMA (Non-Uniform Memory Access), permettant aux processeurs d'accéder plus rapidement à leur mémoire locale, un atout majeur pour les serveurs multi-sockets.

Pointeurs colorés (Colored Pointers)

La technologie clé de ZGC est l'utilisation de bits de métadonnées directement dans le pointeur de l'objet (64 bits). Ces bits indiquent l'état de l'objet :

  • Marked0 / Marked1 : Utilisés pour le marquage concurrent.
  • Remapped : Indique si l'adresse est à jour après un déplacement d'objet.

Barrière de lecture et auto-cicatrisation

Contrairement à G1 qui utilise des barrières d'écriture, ZGC s'appuie sur des barrières de lecture. Lorsqu'un thread accède à un objet, la barrière vérifie les bits de couleur. Si l'objet a été déplacé, le pointeur est mis à jour immédiatement.

// Pseudo-code de la barrière de lecture ZGC
Object charger_reference(Object* champ_source) {
    Object ref = *champ_source;
    if (verif_besoin_reparation(ref)) {
        // L'objet a bougé, on cherche sa nouvelle adresse
        Object nouvelle_ref = obtenir_nouvelle_adresse(ref);
        // "Auto-cicatrisation" : on met à jour le pointeur en mémoire
        *champ_source = nouvelle_ref;
        return nouvelle_ref;
    }
    return ref;
}

3. Shenandoah : L'alternative par pointeurs de transfert

Développé par Red Hat et intégré au JDK 12, Shenandoah partage la philosophie de G1 mais pousse la concurrence plus loin en effectuant l'évacuation (le déplacement des objets) de manière concurrente avec les threads applicatifs.

Pointeurs de Brooks

Pour permettre le déplacement d'un objet alors que l'application l'utilise, Shenandoah utilise les Brooks Pointers. Chaque objet possède un en-tête supplémentaire pointant vers lui-même ou vers sa nouvelle copie si celle-ci a été créée. Les threads passent par ce pointeur pour garantir qu'ils travaillent toujours sur la version la plus récente de l'objet.

Mécanisme Brooks Pointer#### Matrice de connexion

Au lieu des RSet (Remembered Sets) de G1, qui peuvent consommer beaucoup de mémoire, Shenandoah utilise une matrice de connexion pour suivre les références entre régions, simplifiant ainsi la gestion des cycles de collecte.

4. Comparaison des performances

Caractéristique G1 ZGC Shenandoah
Évacuation concurrente Non (STW) Oui Oui
Capacité max du tas ~Moins de 1 To Jusqu'à 16 To Jusqu'à 256 To
Temps de pause cible Variable (ms) < 10ms Ultra-faible
Barrière utilisée Écriture Lecture Lecture/Écriture

Le choix d'un collecteur dépend de la priorité du système : G1 offre un excellent équilibre entre débit et latence pour la plupart des applications, tandis que ZGC et Shenandoah sont indispensables pour les systèmes nécessitant une réactivité constante malgré des volumes de données extrêmement importants.

Étiquettes: JVM garbage-collection G1GC ZGC ShenandoahGC

Publié le 19 juillet à 07h27