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.
#### 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.
#### 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.