Lire les logs GC sans outil externe

Quand le GC pose problème, le réflexe est d’envoyer le gc.log à GCeasy ou de l’ouvrir dans JDK Mission Control. C’est très bien, ces outils sortent des graphes propres en quelques secondes.

Mais le log lui-même se lit. Ce n’est pas du binaire, ce sont des lignes de texte, et chacune raconte ce qui s’est passé pendant une collecte. Une fois qu’on sait les décoder, on comprend le rythme du GC, on repère une fuite ou un Full GC de trop, et on sait dire si le problème vient vraiment du GC ou d’ailleurs. Tout ça sans rien installer, souvent avant même d’ouvrir un profiler.

On va lire une sortie G1 sur JDK 25, ligne par ligne. G1 est le collecteur par défaut, donc c’est ce que vous avez le plus de chances de croiser. Si vous ne savez pas encore quel collecteur choisir ni comment dimensionner la heap, commencez par Régler le GC de la JVM.

Activer les logs

Le logging GC passe par le logging unifié de la JVM. La forme utile :

-Xlog:gc:file=gc.log:time,uptime,level,tags

Une nuance qui change tout : le tag gc seul donne une ligne résumé par collecte, ce qui suffit dans la plupart des cas. Le tag gc* (avec l’étoile) active en plus tous les sous-tags, et sort le détail région par région, les phases internes et le temps CPU. C’est verbeux, mais c’est là qu’on va chercher quand la ligne résumé ne suffit plus :

-Xlog:gc*:file=gc.log:time,uptime,level,tags

Les décorateurs à la fin (time,uptime,level,tags) préfixent chaque ligne : l’horodatage, le nombre de secondes depuis le démarrage de la JVM, le niveau et les tags. L’uptime est précieux, c’est lui qui sert à mesurer les intervalles entre collectes.

La ligne qui résume tout

Voici la ligne produite par le tag gc, une par collecte. C’est le point de départ de toute lecture :

[2026-07-20T10:15:32.412+0200][634.129s][info][gc] GC(148) Pause Young (Normal) (G1 Evacuation Pause) 1804M->216M(4096M) 8.735ms

On la lit de gauche à droite :

Deux chiffres se lisent tout de suite. D’abord ce qui a été libéré : 1804 vers 216, presque 1,6 Go récupéré. Une collecte qui vide bien, c’est le signe d’objets temporaires en majorité, donc du sain. Ensuite la durée de la pause, à comparer à votre budget de latence. 8 ms sur un service web, personne ne le sent. 8 ms sur du trading, c’est une éternité. Tout dépend du contexte.

Les types de pause G1

Le champ après GC(n) dit quel genre de collecte vient d’avoir lieu. Sur G1, les principaux :

Le rythme sain ressemble à ça : beaucoup de Pause Young (Normal), de temps en temps un Concurrent Start suivi de quelques Mixed, et aucun Full GC. Si un Pause Full apparaît en fonctionnement normal, c’est déjà un signal.

Le détail région par région

Quand la ligne résumé ne suffit pas, on passe en gc* et on regarde le découpage. G1 ne gère pas la heap en deux gros blocs, mais en régions de taille fixe. Voici ce qu’il imprime pour la collecte du dessus :

[634.129s][info][gc,heap] GC(148) Eden regions: 800->0(800)
[634.129s][info][gc,heap] GC(148) Survivor regions: 20->24(114)
[634.129s][info][gc,heap] GC(148) Old regions: 80->82
[634.129s][info][gc,heap] GC(148) Humongous regions: 2->2

Ligne par ligne :

Un mot sur la taille des régions. G1 la calcule au démarrage à partir de la taille de la heap, entre 1 et 32 Mo, en visant environ 2048 régions. Sur une heap de 4 Go, ça fait des régions de 2 Mo. D’où le calcul : 800 régions d’eden × 2 Mo = 1600 Mo, ce qui colle avec le 1,6 Go libéré qu’on lisait sur la ligne résumé. Tout est cohérent, et c’est ce qui permet de vérifier une intuition avec une simple multiplication.

Calculer le taux d’allocation à la main

C’est le chiffre le plus utile, et un outil comme GCeasy le calcule pour vous. Mais c’est juste deux soustractions.

Entre deux collectes jeunes, l’eden se remplit entièrement. Il suffit donc de prendre la taille d’eden ramassée et le temps écoulé entre les deux collectes. Reprenons GC(148) à 634.129s, avec ses 1,6 Go d’eden. Disons que la collecte suivante, GC(149), arrive à 635.512s :

Eden collecté : 1600 Mo
Intervalle    : 635.512 - 634.129 = 1.383 s
Taux          : 1600 / 1.383 ≈ 1157 Mo/s, soit ~1,1 Go/s

Voilà votre taux d’allocation. C’est la vitesse à laquelle le code fabrique des objets. Un taux élevé et constant veut dire beaucoup d’objets temporaires créés, donc beaucoup de collectes jeunes, donc du temps grignoté par le GC. Et là, aucun flag ne vous sauvera : le vrai levier est dans le code. Pour voir d’où viennent les allocations, le mode alloc d’async-profiler pointe directement les chemins responsables, c’est le sujet de Profiler la JVM avec async-profiler.

Repérer un problème sans profiler

Voici ce qu’on cherche en parcourant un gc.log, du plus grave au plus discret.

Un Full GC apparaît. La ligne Pause Full (G1 Compaction Pause) ne devrait pas exister en fonctionnement sain :

[812.400s][info][gc] GC(201) Pause Full (G1 Compaction Pause) 3980M->3120M(4096M) 412.006ms

412 ms de pause, et surtout la heap qui reste à 3120 Mo sur 4096 après coup. G1 n’a pas réussi à suivre avec ses collectes Mixed, souvent parce que l’allocation va plus vite que le marquage concurrent, ou parce que la heap est trop petite.

La heap reste pleine après la collecte. Quand le « après » est presque égal au « avant » sur la ligne résumé, par exemple 3900M->3850M(4096M), la collecte n’a quasiment rien libéré. Répété sur plusieurs collectes, c’est soit une heap sous-dimensionnée, soit une fuite mémoire. La suite logique est un OutOfMemoryError, et là il faut passer au heap dump : Diagnostiquer une fuite mémoire avec un heap dump.

Une Evacuation Failure. Elle s’ajoute à la fin de la cause de la collecte, entre parenthèses :

[788.220s][info][gc] GC(178) Pause Young (Normal) (G1 Evacuation Pause) (Evacuation Failure: Allocation) 3900M->3600M(4096M) 95.204ms

G1 a manqué de régions libres pour recopier les survivants pendant la pause. C’est lent et coûteux, souvent le dernier avertissement avant un Full GC. Ça veut dire que la heap est trop pleine ou que la promotion s’emballe.

Les régions humongous grimpent. Beaucoup de régions humongous trahissent de gros tableaux ou de gros buffers. Ils fragmentent la heap et peuvent déclencher des cycles à eux seuls. Souvent, mieux vaut revoir le code qui les alloue que la taille des régions.

Les pauses s’allongent. La durée en fin de ligne résumé qui dépasse votre budget de latence, de façon répétée et pas juste sur un pic isolé.

La pause n’est pas que du GC

Un piège pour finir. La durée affichée sur la ligne gc, c’est le travail du GC. Mais l’application est arrêtée dès l’instant où tous les threads ont atteint un point d’arrêt sûr, le safepoint. Et atteindre ce safepoint prend du temps lui aussi.

Si un thread est coincé dans une longue boucle sans point de sondage, il fait attendre tous les autres. Le GC affiche 8 ms, mais l’application a peut-être été gelée bien plus longtemps. Pour voir ce temps caché :

-Xlog:safepoint:file=safepoint.log:time,uptime

La sortie donne une ligne par safepoint, avec les durées en nanosecondes :

[635.512s] Safepoint "G1CollectForAllocation", Time since last: 219964917 ns, Reaching safepoint: 2208 ns, At safepoint: 8735000 ns, Leaving safepoint: 1625 ns, Total: 8738833 ns, Threads: 0 runnable, 10 total

At safepoint correspond au travail du GC, ici les 8,7 ms qu’on retrouve sur la ligne gc. Reaching safepoint est le temps qu’il a fallu pour arrêter tous les threads avant de commencer. Quand ce dernier gonfle alors que le GC reste court, le problème n’est pas le GC : c’est un thread lent à s’arrêter. On ne le voit jamais si on ne regarde que la ligne gc.

En résumé

Activez les logs avec -Xlog:gc pour l’essentiel, -Xlog:gc* pour le détail. Lisez d’abord la ligne résumé : combien la collecte a libéré, et combien de temps elle a duré. Servez-vous du type de pause pour suivre le rythme, et méfiez-vous du premier Full GC. Descendez dans le détail région par région pour surveiller la promotion et les humongous. Calculez le taux d’allocation à la main, c’est deux soustractions. Et n’oubliez pas que la vraie pause inclut le temps d’atteindre le safepoint. Un gc.log bien lu vous dit où est le problème avant que vous n’ayez ouvert le moindre outil.