Thread dump : utilité, comment faire, différence avec un heap dump

Dans l'article sur le heap dump, je distingue deux symptômes qui se ressemblent mais demandent deux outils différents : une mémoire qui dérive (heap dump) et des threads bloqués sans raison apparente (thread dump). Ce deuxième cas mérite son propre développement, parce que c'est l'outil le plus rapide à utiliser et le plus souvent oublié au profit d'un heap dump qui ne montrera rien d'utile.
Ce qu'un thread dump capture
Un thread dump liste, pour chaque thread actif de la JVM, son état, sa pile d'appels complète au moment du dump, et pour les threads bloqués, l'objet précis (le moniteur) sur lequel il attend et, si l'information est disponible, le thread qui le détient. C'est une photo instantanée de « qui attend qui », pas un historique.
| État du thread | Ce qu'il signifie |
|---|---|
| RUNNABLE | Exécute du code ou attend une ressource système (I/O, réseau) |
| BLOCKED | Attend d'entrer dans une section synchronisée qu'un autre thread occupe |
| WAITING | Attend indéfiniment un signal explicite (Object.wait(), Thread.join()) |
| TIMED_WAITING | Comme WAITING, mais avec un délai maximal (sleep(), wait(timeout)) |
Générer un thread dump
# Recommandé aujourd'hui, ne nécessite pas d'accès à l'installation JDK complète
jcmd <pid> Thread.print
# L'outil historique, toujours largement utilisé
jstack <pid>
# Sans outil JDK à disposition (par exemple dans un conteneur minimal) :
# envoie SIGQUIT, la JVM écrit le dump sur sa sortie standard ou son log
kill -3 <pid>
kill -3 mérite d'être connu séparément des deux autres : dans une image
de conteneur qui n'embarque pas les outils JDK complets, c'est parfois la
seule option disponible sans installer quoi que ce soit, puisqu'elle
s'appuie sur un signal Unix standard plutôt que sur un outil JDK
spécifique.
Le détecteur de deadlock intégré
jstack et jcmd Thread.print analysent automatiquement les threads
bloqués et signalent un cycle d'attente circulaire dans une section dédiée
du dump, typiquement introduite par « Found one Java-level deadlock ».
C'est la partie la plus utile de l'outil : dans le cas le plus fréquent
(deux threads qui se bloquent mutuellement), le dump désigne directement le
cycle, sans qu'il soit nécessaire de reconstruire l'enchaînement à la main.
Found one Java-level deadlock:
=============================
"thread-A":
waiting to lock monitor 0x00007f2b3c003988 (object 0x000000076ab62218, a Account),
which is held by "thread-B"
"thread-B":
waiting to lock monitor 0x00007f2b3c001a58 (object 0x000000076ab62208, a Account),
which is held by "thread-A"
Java stack information for the threads listed above:
"thread-A":
at Account.transfer(Account.java:45)
- waiting to lock <0x000000076ab62218> (a Account)
- locked <0x000000076ab62208> (a Account)
"thread-B":
at Account.transfer(Account.java:45)
- waiting to lock <0x000000076ab62208> (a Account)
- locked <0x000000076ab62218> (a Account)
Ce cas classique vient d'un ordre d'acquisition de verrous incohérent :
thread-A verrouille le compte source puis tente de verrouiller le compte
destination, pendant que thread-B fait exactement l'inverse sur les deux
mêmes comptes. Chacun détient ce que l'autre attend. La correction ne
dépend pas de l'outil : elle demande d'imposer un ordre total d'acquisition
des verrous (par exemple, toujours verrouiller le compte avec l'identifiant
le plus petit en premier), pour qu'un tel cycle devienne structurellement
impossible.
Distinguer un vrai blocage d'une lenteur normale
Un seul thread dump ne suffit pas toujours à conclure : un thread RUNNABLE peut légitimement occuper le CPU longtemps sur un calcul lourd, sans être « bloqué » au sens propre. Le signal fiable est de prendre plusieurs dumps espacés de quelques secondes : si les mêmes threads apparaissent BLOCKED sur les mêmes moniteurs à chaque dump, et que le CPU reste plat pendant ce temps, c'est un vrai blocage. Si un thread RUNNABLE progresse (sa pile d'appels change d'un dump à l'autre), c'est un traitement normal, pas un incident.
Thread dump vs heap dump, en un tableau
| Thread dump | Heap dump | |
|---|---|---|
| Ce qu'il montre | L'état et la pile de chaque thread | Le contenu du tas, objet par objet |
| Bon pour | Deadlocks, threads bloqués, contention de verrous | Fuites mémoire, croissance anormale du tas |
| Coût de génération | Quasi instantané, sans impact notable | Peut geler la JVM plusieurs secondes sur un gros tas |
| Mauvais pour | Diagnostiquer une fuite mémoire | Diagnostiquer un deadlock (aucune information sur les threads) |
Le coût de génération mérite d'être noté : un thread dump n'a quasiment aucun impact sur une application en production, ce qui veut dire qu'il n'y a aucune raison d'hésiter à en prendre plusieurs à la suite pour comparer. Un heap dump, à l'inverse, suspend l'application le temps de sérialiser tout le tas : c'est un geste qui se réserve à un vrai soupçon de fuite, pas un réflexe systématique à chaque ralentissement observé.