GC Doctor Analysez vos logs de ramasse-miettes dans le navigateur Analyser votre log GC dans GC Doctor

English

Glossaire GC

Glossaire GC Java (JVM)

Termes que l'on rencontre dans les logs GC des JVM HotSpot et OpenJ9 (Serial, Parallel, CMS, G1, ZGC, Shenandoah). Chaque entrée dit ce que le terme signifie, comment il apparaît dans un log et ce qu'il implique pour l'application.

Documentation officielle

Allocation Failure (cause de GC)

La cause, dans le log, d'une collecte lancée parce qu'un thread n'a pas pu allouer dans l'espace demandé, le plus souvent parce qu'eden est plein.

Dans les logs de Serial, Parallel et CMS, Pause Young (Allocation Failure) est la collecte jeune ordinaire déclenchée par un eden plein : malgré son nom, rien n'a mal tourné. Pause Full (Allocation Failure) signifie qu'une collecte complète a été nécessaire parce que même la génération ancienne ne pouvait satisfaire une demande, une promotion ou une allocation directe.

Des lignes Pause Full (Allocation Failure) répétées sont un avertissement : le tas est trop petit ou l'application fuit. Si une OutOfMemoryError: Java heap space suit, la dernière collecte complète n'a pas libéré assez.

Documentation officielle : JEP 271: Unified GC Logging

Barrière d'écriture

Code exécuté par l'application chaque fois qu'elle stocke une référence d'objet dans le tas. Il enregistre la modification pour que le collecteur n'ait pas à tout reparcourir.

Les collecteurs générationnels utilisent une barrière d'écriture pour entretenir l'ensemble mémorisé : quand un objet ancien reçoit une référence vers un objet jeune, la barrière marque une carte (une petite plage de mémoire) comme sale, si bien que la collecte jeune suivante ne parcourt que les cartes sales au lieu de toute la génération ancienne. G1 utilise aussi une barrière pour le marquage concurrent (SATB, instantané au début) qui enregistre les références écrasées pendant le marquage, pour ne pas perdre d'objets vivants.

Le surcoût est faible par écriture mais constant, et le raffinement des cartes sales dans G1 consomme des threads d'arrière-plan. Dans les logs GC il se voit dans le temps de mise à jour de l'ensemble mémorisé pendant les pauses et dans le raffinement concurrent.

Barrière de lecture (ZGC, Shenandoah)

Code inséré quand l'application lit une référence d'objet, pour que le collecteur trouve ou corrige une référence vers un objet qui a pu être déplacé.

Le compactage concurrent signifie que le collecteur déplace les objets pendant que les threads tournent : une référence lue dans le tas peut donc être périmée. La barrière vérifie la référence (ZGC code un état dans des bits du pointeur, Shenandoah regarde si l'objet est dans l'ensemble de collecte) et, au besoin, déplace l'objet ou cherche sa nouvelle adresse. Elle répare ensuite la référence dans le tas (auto-guérison), si bien que les lectures suivantes sont rapides.

Le coût est un petit surcoût constant à chaque lecture de référence, en échange de pauses qui ne dépendent pas de la taille du tas. C'est une des raisons pour lesquelles le débit de ZGC et de Shenandoah est un peu inférieur à celui de Parallel ou G1 sur la même charge. Les barrières n'apparaissent pas dans les logs GC ; leur effet se voit dans la consommation de CPU.

Documentation officielle : The Z Garbage Collector (Oracle), JEP 379: Shenandoah, a Low-Pause-Time Garbage Collector (Production)

Collecte mixte (G1)

Pause G1 qui évacue les régions jeunes plus une sélection de régions anciennes contenant le plus de déchets.

Après un cycle de marquage concurrent, G1 sait combien de données vivantes contient chaque région ancienne. Pendant la phase de récupération qui suit, chaque Pause Young (Mixed) ajoute à l'ensemble de collecte quelques régions anciennes parmi les plus riches en déchets, les moins chères à évacuer. La phase se termine quand la fraction récupérable tombe sous -XX:G1HeapWastePercent (5 pour cent par défaut) ou quand le nombre prévu de collectes mixtes (-XX:G1MixedGCCountTarget, 8) est atteint. L'objectif de durée de pause limite le nombre de régions de chaque pause.

Les collectes mixtes sont la façon dont G1 récupère la génération ancienne sans collecte complète. Si la génération ancienne grossit plus vite que les collectes mixtes ne la libèrent, G1 finit par subir des échecs d'évacuation et des collectes complètes. Des pauses mixtes longues indiquent souvent des régions anciennes avec beaucoup de données vivantes ou de grands ensembles mémorisés.

Documentation officielle : Garbage-First (G1) Garbage Collector (Oracle)

Concurrent mode failure (CMS)

CMS uniquement. La génération ancienne s'est remplie avant la fin du cycle concurrent, si bien que la JVM a arrêté l'application et lancé une collecte complète avec compactage.

CMS devait démarrer son cycle concurrent assez tôt pour finir avant que la promotion remplisse la génération ancienne (il démarre à l'occupation fixée par -XX:CMSInitiatingOccupancyFraction, adaptée par défaut). Si la promotion allait plus vite que le cycle, ou si la fragmentation ne laissait aucun bloc libre assez grand pour un objet promu, la JVM écrivait concurrent mode failure et retombait sur une collecte complète séquentielle, avec des pauses de plusieurs secondes sur de grands tas.

CMS a été déprécié en JDK 9 et supprimé en JDK 14 : ce message n'apparaît donc que dans d'anciens logs. Abaisser le seuil de démarrage, agrandir le tas ou passer à G1 sont les remèdes habituels. Les collecteurs modernes ont le même échec sous d'autres noms : échec d'évacuation (G1), GC dégénéré (Shenandoah), allocation stall (ZGC).

Débit

La part du temps que l'application passe à travailler utilement plutôt qu'en pauses GC.

Un débit de 99 pour cent signifie une seconde de pauses pour 100 secondes. Le réglage orienté débit (Parallel, grands tas) accepte des pauses plus longues et plus rares. Le réglage orienté latence (ZGC, Shenandoah, G1 avec un objectif de pause bas) accepte un travail plus fréquent et plus court, et dépense plus de CPU dans le collecteur.

GC Doctor calcule le débit à partir des pauses de la plage sélectionnée. Il ignore le travail concurrent, qui consomme du CPU sans arrêter l'application : lisez-le avec la vue CPU.

Débit d'allocation

Le nombre d'octets alloués par seconde par l'application. Il détermine la fréquence des collectes jeunes.

Entre deux collectes jeunes, la quantité allouée égale l'espace libre d'eden. Pour une taille d'eden donnée, doubler le débit d'allocation divise par deux l'intervalle entre collectes. Un débit élevé n'est pas un défaut en soi, puisque les objets de courte durée coûtent peu, mais il multiplie les collectes, laisse moins de temps aux objets pour mourir (donc plus sont promus) et, avec les collecteurs concurrents, peut distancer le collecteur.

GC Doctor déduit le débit du tas avant chaque collecte et après la précédente. Pour le réduire : réutiliser les objets, éviter le boxing et les chaînes temporaires, dimensionner les collections à l'avance, et profiler les allocations avec Java Flight Recorder (jdk.ObjectAllocationSample).

Désoptimisation

La JVM abandonne du code compilé et revient à l'interpréteur parce qu'une hypothèse faite par le compilateur JIT n'est plus valide.

Le compilateur JIT optimise agressivement grâce au profilage à l'exécution, par exemple en supposant qu'une seule classe implémente une interface ou qu'une branche n'est jamais prise. Quand l'hypothèse tombe (une nouvelle classe est chargée, un piège rare est atteint), les frames compilées sont converties en frames d'interpréteur. Les désoptimisations massives s'exécutent à un safepoint et apparaissent dans -Xlog:safepoint comme des opérations Deoptimize.

Ce n'est pas du ramasse-miettes, mais elles arrêtent l'application et entrent donc dans la même analyse des pauses. Des désoptimisations fréquentes après le démarrage suggèrent des classes chargées à l'exécution ou des chemins de code qui changent sans cesse.

Échec d'évacuation / to-space exhausted (G1)

G1 n'a pas pu copier un objet vivant faute de région libre pour le recevoir. Le log affiche to-space exhausted.

Pendant une collecte jeune ou mixte, G1 copie (évacue) les objets vivants vers des régions libres. S'il en manque, il ne peut pas tout déplacer : il laisse sur place les objets qu'il n'a pas pu copier, termine la pause en réparant les références et conserve ces régions. Une telle pause est bien plus longue que d'habitude, et une collecte complète suit souvent, la plus lente de toutes.

Causes : trop peu de tas libre au début de la collecte (le cycle concurrent a démarré trop tard, le débit d'allocation est élevé, beaucoup d'objets humongous), ou un tas trop petit pour les données vivantes. Remèdes : agrandir le tas, abaisser -XX:InitiatingHeapOccupancyPercent ou laisser l'IHOP adaptatif activé pour que le cycle concurrent démarre plus tôt, augmenter -XX:G1ReservePercent (10 par défaut) et réduire les allocations humongous.

Documentation officielle : Garbage-First (G1) Garbage Collector (Oracle), Garbage-First Garbage Collector Tuning (Oracle)

Eden

Zone de la génération jeune où les nouveaux objets sont alloués en premier. Quand elle est pleine, une collecte jeune démarre.

Les threads de l'application allouent dans eden en avançant un pointeur dans leur propre tampon d'allocation local (TLAB), ce qui rend l'allocation très bon marché. Quand un thread ne peut plus obtenir d'espace dans eden, la JVM arrête le monde et lance une collecte jeune (cause Allocation Failure avec Serial et Parallel, G1 Evacuation Pause avec G1).

Le temps entre deux collectes jeunes est la taille d'eden divisée par le débit d'allocation. Un eden de 512 Mio et une application qui alloue 256 Mio par seconde collectent toutes les deux secondes. Un eden plus grand réduit la fréquence des collectes ; il ne raccourcit pas chaque pause.

Ensemble de données vivantes

La mémoire dont l'application a vraiment besoin : ce qui reste accessible après une collecte complète. Il fixe la taille de tas minimale utile.

Mesurez-le comme l'occupation du tas juste après une collecte complète ou ancienne. Après une collecte jeune, la génération ancienne peut encore contenir des déchets, donc on le surestime. En règle empirique, un tas de deux à quatre fois l'ensemble de données vivantes laisse assez de place à l'allocation. Plus le tas est petit, plus le collecteur tourne souvent et consomme de CPU ; plus il est grand, moins souvent, mais les pauses de certains collecteurs s'allongent.

Un ensemble de données vivantes qui grossit pendant des heures sans se stabiliser est la signature d'une fuite mémoire ; un ensemble stable mais proche de la taille du tas indique un tas trop petit. GC Doctor montre le tas après chaque collecte et signale une augmentation continue.

Ensemble mémorisé

Structure qui enregistre quelles parties du tas contiennent des références vers une zone donnée, pour pouvoir collecter cette zone sans parcourir tout le reste.

Un collecteur générationnel ne peut collecter la génération jeune seule que s'il connaît tous les objets anciens qui pointent vers elle. Au lieu de parcourir la génération ancienne, il consulte l'ensemble mémorisé (une table de cartes dans Serial et Parallel, un ensemble par région dans G1) que la barrière d'écriture entretient.

Dans G1, les ensembles mémorisés permettent aussi de collecter n'importe quel sous-ensemble de régions anciennes, ce qui rend les collectes mixtes possibles. Ils sont le principal surcoût mémoire de G1 (quelques pour cent du tas, davantage avec beaucoup de références entre régions), et leur parcours prend du temps dans chaque pause (Scan Heap Roots). Leur reconstruction est une phase concurrente après le marquage (Concurrent Rebuild Remembered Sets).

Espace survivor

Petite zone de la génération jeune, en deux moitiés, qui contient les objets ayant survécu à au moins une collecte jeune mais pas assez âgés pour être promus.

À chaque collecte jeune, les survivants sont copiés depuis eden et depuis l'ancien espace survivor vers l'autre, et leur âge augmente de un. Les garder dans la génération jeune laisse aux objets de courte durée une chance de plus de mourir avant la promotion. G1 gère les régions survivor comme un ensemble plutôt que comme deux moitiés fixes.

La taille de l'espace survivor limite le nombre d'objets qui peuvent rester jeunes : si les survivants le débordent, l'excédent est promu immédiatement. Avec le tag gc+age=trace, le log indique combien d'octets ont chaque âge ; beaucoup d'octets aux âges les plus élevés signifie que les objets sont en route vers la génération ancienne. GC Doctor dessine cette distribution en carte de chaleur quand le log la contient.

Full GC

Collecte stop-the-world de tout le tas : marquage puis compactage. La collecte la plus lente ; dans une application saine elle est rare ou absente.

Déclencheurs typiques : plus de place pour promouvoir ou allouer après une collecte jeune (Allocation Failure), un appel explicite à System.gc(), le seuil du metaspace, une demande d'inspection ou de dump du tas, un collecteur concurrent qui a pris du retard. Le coût croît avec les données vivantes, puisqu'il faut toutes les marquer et les déplacer : des secondes sur un tas de plusieurs gigaoctets.

Serial et Parallel collectent leur génération ancienne par une collecte complète par conception. G1 utilise une collecte complète parallèle depuis JDK 10, mais elle reste un dernier recours. Le signe d'un problème, ce sont des collectes complètes fréquentes qui récupèrent peu : l'ensemble de données vivantes est proche de la taille du tas, et l'application fuit ou est sous-dimensionnée. Lisez la cause dans la ligne de log, Pause Full (cause), puis le tas après la collecte. GC Doctor liste chaque full GC avec sa cause et son tas avant et après.

GC dégénéré (Shenandoah)

Quand un cycle concurrent ne suit pas l'allocation, Shenandoah arrête l'application et termine le cycle dans une pause.

Shenandoah travaille normalement en concurrence. Si l'application alloue plus vite que le collecteur libère de la mémoire, la mémoire libre s'épuise. Shenandoah ralentit d'abord les threads qui allouent (pacing) puis, si cela ne suffit pas, passe à un cycle dégénéré : la fin du cycle concurrent s'exécute dans une pause stop-the-world. Si cela ne libère toujours pas assez, il retombe sur une collecte complète.

Une ligne de log comme Pause Degenerated GC (Mark) nomme la phase où le cycle a dégénéré. Des cycles dégénérés fréquents signifient que le tas est trop petit pour le débit d'allocation ou que le cycle démarre trop tard : donnez plus de marge au collecteur avec un tas plus grand, ou changez les heuristiques (-XX:ShenandoahGCHeuristics). ZGC rencontre la même situation sous la forme d'un allocation stall, où le thread qui alloue attend que de la mémoire soit libérée.

Documentation officielle : JEP 379: Shenandoah, a Low-Pause-Time Garbage Collector (Production)

GC explicite (System.gc())

Collecte demandée par du code ou un outil plutôt que par le collecteur lui-même, avec la cause System.gc() ou Diagnostic Command.

System.gc(), Runtime.gc() et certaines bibliothèques (nettoyage des tampons directs NIO, GC distribué de RMI, qui tourne toutes les heures par défaut) déclenchent une collecte complète, affichée Pause Full (System.gc()). Les outils d'inspection du tas comme jmap -histo:live et jcmd GC.run en déclenchent aussi une. Les collectes explicites sont stop-the-world et peuvent durer des secondes sur un grand tas.

Elles sont rarement utiles. -XX:+DisableExplicitGC les ignore ; avec G1 ou CMS, -XX:+ExplicitGCInvokesConcurrent les transforme en cycles concurrents. Des pauses qui reviennent à intervalle fixe, une heure d'écart, viennent généralement de RMI.

Documentation officielle : The java command (Oracle)

GC overhead limit exceeded

Une OutOfMemoryError levée quand la JVM passe presque tout son temps en GC et ne récupère presque rien.

Parallel GC lève java.lang.OutOfMemoryError: GC overhead limit exceeded quand plus de 98 pour cent du temps est passé en GC et moins de 2 pour cent du tas est récupéré, sur plusieurs collectes complètes consécutives (-XX:+UseGCOverheadLimit, activé par défaut). C'est un symptôme, pas une cause : l'ensemble de données vivantes dépasse le tas, ou une fuite continue de le remplir.

Cherchez dans le log des lignes Pause Full enchaînées qui ne récupèrent presque rien, puis analysez un heap dump (jcmd GC.heap_dump) pour trouver ce qui retient la mémoire. Augmenter -Xmx retarde l'échec mais ne corrige pas une fuite.

Documentation officielle : Troubleshoot Memory Leaks (Oracle)

Génération ancienne

Là où sont installés les objets qui ont survécu à plusieurs collectes jeunes. Elle est collectée rarement, et plus cher, car l'essentiel de son contenu est vivant.

L'occupation de la génération ancienne juste après chaque collecte ancienne est le meilleur signal d'une fuite ou d'une promotion prématurée. Si elle grimpe sans cesse, l'application retient de plus en plus de données (fuite, cache non borné). Si elle monte par paliers, des données de courte durée sont promues trop tôt, souvent parce que l'espace survivor est trop petit ou que le débit d'allocation est élevé.

Les collecteurs récupèrent la génération ancienne de manières différentes : Serial et Parallel la compactent lors d'une collecte complète stop-the-world ; CMS (supprimé en JDK 14) la balayait en concurrence ; G1 la marque en concurrence puis la récupère par petites étapes avec des collectes mixtes ; ZGC et Shenandoah collectent tout le tas en concurrence.

Génération jeune

Partie du tas où les nouveaux objets sont alloués : un espace eden plus des espaces survivor (dans G1, un ensemble de régions). Elle est collectée souvent car la plupart des objets meurent jeunes.

L'hypothèse générationnelle faible dit que la plupart des objets deviennent inaccessibles peu après leur allocation. Une collecte jeune l'exploite : elle ne regarde que la génération jeune, copie les rares survivants dans un espace survivor ou les promeut dans la génération ancienne, et récupère tout le reste d'un coup. Son coût est proportionnel aux survivants, pas aux déchets.

Les collectes jeunes sont stop-the-world dans Serial, Parallel et G1 ; ZGC générationnel les fait en concurrence. Une génération jeune plus grande donne moins de collectes et plus de temps aux objets pour mourir avant d'être promus. G1 la redimensionne seul pour respecter son objectif de pause (-XX:MaxGCPauseMillis, 200 ms par défaut).

Documentation officielle : Available collectors (Oracle)

IHOP (G1)

Initiating Heap Occupancy Percent : l'occupation de la génération ancienne à laquelle G1 démarre un cycle de marquage concurrent.

Un cycle concurrent doit finir avant que la génération ancienne soit pleine. S'il démarre trop tard, G1 subit des échecs d'évacuation ou une collecte complète ; s'il démarre trop tôt, le collecteur tourne plus souvent que nécessaire et gaspille du CPU. Par défaut G1 adapte le seuil automatiquement (IHOP adaptatif) d'après la durée de marquage et le débit d'allocation observés, avec -XX:InitiatingHeapOccupancyPercent (45) comme valeur initiale. -XX:-G1UseAdaptiveIHOP donne un seuil fixe.

Une pause Concurrent Start (cause G1 Evacuation Pause ou G1 Humongous Allocation) marque le début d'un cycle. Si les cycles démarrent juste après la fin du précédent, la génération ancienne est sous pression.

Documentation officielle : Garbage-First (G1) Garbage Collector (Oracle), Garbage-First Garbage Collector Tuning (Oracle)

Latence

Ce que le GC coûte à une requête : les pauses qu'il ajoute au temps de réponse, surtout en queue de distribution.

Les moyennes cachent le problème. Un collecteur dont la pause moyenne est de 1 ms peut quand même produire une pause de 500 ms une fois par minute, et c'est ce que montre le 99e centile de la latence côté client. Regardez la pause maximale et le 99e centile des pauses, puis la fréquence des pauses longues. Les effets cumulés comptent aussi : plusieurs pauses dans la même requête s'additionnent.

La pause rapportée dans le log GC est un minorant de l'arrêt vu par l'application, car le temps pour atteindre un safepoint et celui pour relancer les threads sont hors de cette mesure.

Marquage et compactage

Les deux étapes d'une collecte complète : trouver tous les objets vivants (marquage), puis les tasser pour créer une zone libre contiguë (compactage).

Le marquage parcourt le graphe d'objets depuis les racines. Le compactage supprime la fragmentation : ensuite l'allocation est un simple avancement de pointeur et les gros objets trouvent de la place. Le prix est qu'il faut mettre à jour les références vers chaque objet déplacé, donc arrêter tous les threads.

Serial et Parallel utilisent marquage et compactage pour la génération ancienne. G1 et les collecteurs concurrents évitent de compacter tout le tas d'un coup en n'évacuant que certaines régions. Le balayage sans compactage (CMS, Go) coûte moins cher mais laisse l'espace libre fragmenté.

Metaspace

Mémoire native, hors du tas Java, qui contient les métadonnées des classes chargées : structures, méthodes et pools de constantes. Il a remplacé la génération permanente en JDK 8.

Le metaspace grandit quand des classes sont chargées et rétrécit quand leur chargeur de classes est ramassé. Par défaut il n'est limité que par la mémoire native disponible. -XX:MaxMetaspaceSize fixe un plafond, et l'atteindre lève OutOfMemoryError: Metaspace. -XX:MetaspaceSize fixe le seuil auquel la première collecte est déclenchée (cause Metadata GC Threshold dans le log).

Causes typiques de croissance : des frameworks qui génèrent des classes à l'exécution (proxys, génération de bytecode, scripts), des redéploiements qui laissent fuir des chargeurs de classes, ou une limite trop basse pour le nombre de classes. Une dent de scie avec des collectes Metadata GC Threshold signifie que le seuil est trop bas et que chaque augmentation déclenche une collecte complète : augmentez MetaspaceSize. Depuis JDK 16 (JEP 387), le metaspace inutilisé est rendu plus vite au système.

Documentation officielle : JEP 387: Elastic Metaspace, The java command (Oracle)

Objet humongous (G1)

Dans G1, objet dont la taille atteint au moins la moitié d'une région. Il est alloué directement dans des régions anciennes contiguës et évite la génération jeune.

G1 découpe le tas en régions égales de 1 à 32 Mio (une puissance de deux choisie selon la taille du tas ; -XX:G1HeapRegionSize la remplace). De grands tableaux, tampons ou chaînes peuvent dépasser la moitié d'une région. Ils occupent alors entièrement une ou plusieurs régions contiguës, et la fin inutilisée de la dernière est perdue.

Cela pose trois problèmes : la fragmentation (il faut des régions libres contiguës), les cycles concurrents prématurés (une allocation humongous peut déclencher une collecte Concurrent Start ; la cause dans le log est G1 Humongous Allocation) et, quand le tas en est presque plein, des échecs d'évacuation ou des collectes complètes. Les objets humongous morts peuvent être récupérés tôt pendant les collectes jeunes, mais ceux qui vivent longtemps restent jusqu'à un cycle concurrent ou une collecte mixte.

Remèdes : augmenter la taille des régions, découper les gros tampons ou les réutiliser. La vue Humongous de GC Doctor liste les collectes concernées.

Documentation officielle : Garbage-First (G1) Garbage Collector (Oracle)

Parcours des racines

Recherche des points de départ de l'accessibilité : piles des threads, champs statiques, handles JNI, registres et autres structures du runtime.

Toute collecte commence par les racines, puisqu'un objet n'est vivant que s'il est accessible depuis elles. Le temps croît avec le nombre de threads et la profondeur de leurs piles, avec le nombre de classes chargées (racines des chargeurs de classes) et avec le nombre de références JNI. Dans les logs de G1 cela apparaît sous Ext Root Scanning, avec une valeur par thread de travail.

Un long parcours des racines avec peu de données vivantes indique de nombreux threads, de très grosses piles ou beaucoup de classes. ZGC et Shenandoah parcourent les piles des threads en concurrence, ce qui sort ce travail de la pause.

Pause / Stop-the-world

Période pendant laquelle tous les threads de l'application sont arrêtés pour que le collecteur travaille sur un tas qui ne bouge pas sous ses pieds.

Les pauses sont ce que les utilisateurs ressentent comme de la latence : une requête qui arrive pendant une pause attend toute la pause. Une ligne de log comme Pause Young (Normal) (G1 Evacuation Pause) 24M à 4M (256M) 3.2ms donne la cause, le tas avant et après la collecte, la taille de tas allouée et la durée.

Tout le travail de collecte n'est pas une pause. Les collecteurs modernes (G1, ZGC, Shenandoah) font l'essentiel du marquage en concurrence et gardent les phases stop-the-world courtes, mais certaines étapes en exigent toujours une : démarrer et terminer un cycle de marquage, évacuer les objets vivants dans G1, ou une collecte complète de secours. La durée d'une pause croît avec la quantité de données vivantes à copier ou parcourir, pas avec la quantité de déchets ; ZGC et Shenandoah font exception, leurs pauses ne dépendent presque pas de la taille du tas.

GC Doctor trace chaque pause dans le temps et indique la plus longue, le 99e centile et la part du temps passée en pause.

Documentation officielle : JEP 271: Unified GC Logging

Phases concurrentes

Travail du collecteur effectué pendant que les threads de l'application continuent de tourner, comme le marquage des objets vivants. Il doit se terminer avant que le tas soit plein, sinon le collecteur retombe sur une collecte stop-the-world.

Le travail concurrent échange du CPU contre des pauses plus courtes : les threads GC se disputent les cœurs avec l'application, et les barrières insérées dans le code applicatif (barrières d'écriture et de lecture) ajoutent un petit surcoût constant. G1 marque la génération ancienne en concurrence, ZGC et Shenandoah déplacent aussi les objets en concurrence, CMS (supprimé en JDK 14) marquait et balayait en concurrence.

Dans un log, les phases concurrentes apparaissent avec leurs propres durées (Concurrent Mark Cycle, Concurrent Mark, Concurrent Rebuild Remembered Sets...). Leur durée ne s'ajoute pas aux pauses. Elles comptent de deux façons : un cycle trop long laisse le tas se remplir (voir échec d'évacuation, concurrent mode failure et GC dégénéré), et des cycles enchaînés consomment du CPU. GC Doctor indique la part du temps passée en travail concurrent et son recouvrement avec les pauses.

Pointeur de redirection

Pointeur ou table qui indique où vit désormais un objet déplacé, pour que les threads trouvent la nouvelle copie.

Pendant l'évacuation d'une région, l'ancienne et la nouvelle copie d'un objet coexistent. L'information de redirection dit laquelle est la bonne : Shenandoah la stocke dans l'objet lui-même, ZGC garde une table de redirection par page déplacée. Une fois toutes les références vers les objets déplacés mises à jour, l'ancienne région est libérée et l'information de redirection est jetée.

Cette information demande de la mémoire propre, qui fait partie du surcoût du collecteur et explique en partie pourquoi ZGC et Shenandoah ont besoin d'une marge au-delà des données vivantes.

Promotion / Vieillissement

Déplacement d'un objet de la génération jeune vers l'ancienne, parce qu'il a survécu à assez de collectes jeunes ou parce que l'espace survivor était plein.

Chaque survivant d'une collecte jeune prend un an. Le seuil de vieillissement (-XX:MaxTenuringThreshold, 15 par défaut) est l'âge à partir duquel les objets sont promus. Parallel et G1 l'abaissent dynamiquement pour que les survivants tiennent dans l'espace survivor. S'ils n'y tiennent pas, l'excédent est promu quel que soit son âge : c'est la promotion prématurée.

La promotion a deux coûts. L'objet est copié une fois de plus, et il mourra désormais dans la génération ancienne, où une collecte plus coûteuse le récupérera. Un volume promu élevé par rapport au débit d'allocation, ou des collectes anciennes qui récupèrent beaucoup, suggèrent une génération jeune trop petite ou des survivants trop gros. GC Doctor trace les octets promus par chaque collecte.

Région

Bloc de taille fixe du tas. Les collecteurs à régions (G1, ZGC, Shenandoah) récupèrent la mémoire région par région plutôt qu'en une génération contiguë.

G1 divise le tas en environ 2048 régions de taille égale (1 à 32 Mio). Chaque région reçoit un rôle selon les besoins : eden, survivor, ancienne ou humongous. G1 peut ainsi choisir quelles régions collecter pour respecter son objectif de pause. ZGC utilise des pages de 2 Mio (petits objets), 32 Mio (moyens) ou plus grandes pour les gros objets, et Shenandoah divise aussi le tas en régions.

Une organisation en régions évite une séparation fixe entre jeune et ancien et permet un compactage partiel. Le revers est le gaspillage interne quand les objets ne remplissent pas les régions, comme avec les objets humongous de G1.

Documentation officielle : Garbage-First (G1) Garbage Collector (Oracle), The Z Garbage Collector (Oracle)

Safepoint

Point de l'exécution des threads Java où leur état est entièrement décrit, ce qui permet à la JVM de l'inspecter ou de le modifier sans risque.

Une opération de la VM (collecte stop-the-world, heap dump, désoptimisation de nombreuses méthodes, thread dump, révocation de verrou biaisé avant JDK 18) demande un safepoint. Chaque thread Java s'exécute jusqu'à un point de contrôle et s'y parque, l'opération s'exécute, puis les threads reprennent. Le délai entre la demande et le moment où le dernier thread atteint le safepoint est le time to safepoint, et un log GC qui ne l'enregistre pas le cache dans la pause rapportée.

Un time to safepoint long vient typiquement d'un thread bloqué dans une longue boucle comptée sans point de contrôle, ou d'un thread lent à être ordonnancé. Activez -Xlog:safepoint pour le mesurer. Quand l'application se fige plus longtemps que la pause du log GC, regardez le temps de safepoint. La vue Safepoints montre chaque opération, son time to safepoint et sa durée.

Documentation officielle : JEP 271: Unified GC Logging

Seuil de vieillissement

L'âge, compté en collectes jeunes survécues, à partir duquel un objet est promu dans la génération ancienne.

-XX:MaxTenuringThreshold fixe le maximum (15 par défaut). À chaque collecte jeune, le collecteur calcule un seuil dynamique : si les survivants d'âges jusqu'à n occupent plus que le ratio cible (-XX:TargetSurvivorRatio, 50 pour cent) de l'espace survivor, le seuil devient n.

Un seuil qui tombe à 1 montre que les survivants débordent : les objets sont promus après une seule survie, c'est la promotion prématurée. Un seuil qui reste à 15 avec peu de survivants est le cas sain. GC Doctor trace le seuil et la distribution des âges quand le log les fournit.

TLAB

Tampon d'allocation local à un thread : une tranche privée d'eden confiée à chaque thread pour que l'allocation ne demande aucun verrou.

Un thread alloue en avançant un pointeur dans son TLAB. Ce n'est que lorsque le tampon est épuisé qu'il en demande un nouveau à eden partagé, chemin plus lent avec synchronisation. La JVM dimensionne les TLAB de façon adaptative d'après le débit d'allocation de chaque thread. Un objet plus grand que l'espace restant du TLAB est alloué hors de celui-ci, directement dans eden.

Avec -Xlog:gc+tlab=trace, le log montre l'allocation et le gaspillage par thread. Les événements JFR ObjectAllocationInNewTLAB et ObjectAllocationOutsideTLAB indiquent d'où viennent les allocations ; celles hors TLAB sont plus lentes et typiques des grands tableaux.

Verrou biaisé

Ancienne optimisation qui supposait qu'un verrou n'est utilisé que par un seul thread, rendant son acquisition presque gratuite. Désactivée par défaut en JDK 15 et supprimée en JDK 18.

Le premier thread à prendre un verrou marquait l'objet comme biaisé en sa faveur. Un autre thread ne pouvait prendre le verrou qu'en révoquant le biais, ce qui exigeait un safepoint. Les révocations sont devenues coûteuses et le gain faible, les applications s'éloignant des collections synchronisées héritées (JEP 374).

Dans d'anciens logs de JVM, des safepoints nommés RevokeBias ou BulkRevokeBias peuvent expliquer des pauses qu'aucune collecte ne justifie. Avec JDK 8 à 14, -XX:-UseBiasedLocking les supprime.

Documentation officielle : JEP 374: Deprecate and Disable Biased Locking