jcmd : les commandes à connaître en production
Un service de facturation tourne dans un conteneur, avec l’image eclipse-temurin:25-jdk. Depuis ce matin, sa latence monte et sa heap grossit sans arrêt. Aucun profiler n’est installé dans l’image. On ouvre un shell dans le conteneur. Le JDK contient déjà l’outil qu’il faut : jcmd.
$ jcmd
1 Billing
40 jdk.jcmd/sun.tools.jcmd.JCmd
Sans argument, jcmd liste les JVM de l’utilisateur courant, avec leur pid et leur classe principale. Dans un conteneur, la JVM est souvent le processus 1. La deuxième ligne est jcmd lui-même, qui est aussi une JVM.
Cet article présente les commandes jcmd utiles quand on est de garde, avec leur sortie réelle et leur coût pour l’application. Les tests ont été faits avec OpenJDK 25.0.4.1 (Corretto) sur un Mac. Les tests en conteneur ont tourné dans eclipse-temurin:25-jdk (Temurin 25.0.4.1+1). L’application de test, Billing, reproduit trois problèmes. Un cache ne se vide jamais, huit threads se disputent un lock, et cinquante virtual threads attendent.
Comment jcmd fonctionne
jcmd envoie une commande de diagnostic à une JVM en cours d’exécution, puis affiche la réponse. La JVM exécute la commande elle-même, dans un thread dédié, l’Attach Listener. Il n’y a rien à installer et pas besoin de redémarrer la JVM.
La forme générale est jcmd <pid> <commande> [options]. Le pid peut être remplacé par le nom de la classe principale :
$ jcmd Billing VM.uptime
42500:
237.640 s
41959:
433.878 s
Avec un nom de classe, la commande s’exécute sur toutes les JVM qui portent ce nom. Ici, deux instances de Billing tournaient, et les deux ont répondu. Sur une machine qui héberge plusieurs instances du même service, utilisez le pid.
jcmd accepte une seule commande par appel. jcmd <pid> VM.uptime VM.version répond Unknown argument 'VM.version' in diagnostic command. Pour lancer plusieurs commandes à la suite, écrivez-les dans un fichier, une par ligne, et lancez jcmd <pid> -f commandes.txt.
La liste des commandes vient de la JVM elle-même :
$ jcmd 41959 help
41959:
The following commands are available:
AOT.end_recording
Compiler.CodeHeap_Analytics
Compiler.codecache
...
GC.class_histogram
GC.finalizer_info
GC.heap_dump
GC.heap_info
GC.run
...
JFR.view
JVMTI.agent_load
...
VM.set_flag
VM.stringtable
VM.symboltable
VM.system_properties
VM.systemdictionary
VM.uptime
VM.version
help
Sur JDK 25, il y en a 54 sur macOS et 57 sur Linux, sans compter help. Linux ajoute Compiler.perfmap, System.native_heap_info et System.trim_native_heap. Chaque commande a une aide détaillée, avec une ligne Impact qui dit ce qu’elle coûte à l’application :
$ jcmd 41959 help GC.heap_dump
41959:
GC.heap_dump
Generate a HPROF format dump of the Java heap.
Impact: High: Depends on Java heap size and content. Request a full GC unless the '-all' option is specified.
Syntax : GC.heap_dump [options] <filename>
Arguments:
filename : Name of the dump file (FILE, no default value)
Options: (options must be specified using the <key> or <key>=<value> syntax)
-all : [optional] Dump all objects, including unreachable objects (BOOLEAN, false)
-gz : [optional] If specified, the heap dump is written in gzipped format using the given compression level. 1 (recommended) is the fastest, 9 the strongest compression. (INT, 1)
-overwrite : [optional] If specified, the dump file will be overwritten if it exists (BOOLEAN, false)
-parallel : [optional] Number of parallel threads to use for heap dump. The VM will try to use the specified number of threads, but might use fewer. (INT, 1)
Lisez la ligne Impact avant de lancer une commande sur une JVM sous charge.
Savoir ce qui tourne vraiment
Quatre commandes donnent l’état de départ : la version, le temps de fonctionnement, la ligne de commande et les flags.
$ jcmd 41959 VM.version
41959:
OpenJDK 64-Bit Server VM version 25.0.4.1+10-LTS
JDK 25.0.4.1
$ jcmd 41959 VM.uptime
41959:
433.878 s
$ jcmd 41959 VM.command_line
41959:
VM Arguments:
jvm_args: -Xmx1g -XX:+UseG1GC -Xlog:safepoint:file=/tmp/safepoint.log:uptime -Xlog:gc:file=/tmp/gc.log:uptime
java_command: Billing
java_class_path (initial): classes
Launcher Type: SUN_STANDARD
VM.command_line montre les arguments que la JVM a vraiment reçus. Les options venues de la variable JAVA_TOOL_OPTIONS y apparaissent aussi, au début de jvm_args. C’est utile quand un script de démarrage, une variable d’environnement et une image Docker ajoutent chacun leurs options.
VM.flags affiche les flags dont la valeur n’est pas celle par défaut, y compris ceux que la JVM a choisis elle-même :
$ jcmd 41959 VM.flags
41959:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=9 ... -XX:InitialHeapSize=268435456 ... -XX:MaxHeapSize=1073741824 -XX:MaxNewSize=643825664 ... -XX:+UseCompressedOops -XX:+UseG1GC ...
On y lit la taille réelle de la heap, le nombre de threads GC et le nombre de threads de compilation. Personne n’a écrit ConcGCThreads=2 sur la ligne de commande : la JVM l’a calculé à partir des CPU visibles. En conteneur, c’est la limite CPU qui détermine ce calcul.
VM.flags -all liste tous les flags, 505 sur ce JDK 25, avec leur valeur et leur origine. Un filtre sur manageable donne ceux qu’on peut changer sans redémarrer :
$ jcmd 41959 VM.flags -all | grep manageable
uint FullGCHeapDumpLimit = 0 {manageable} {default}
uintx G1PeriodicGCInterval = 0 {manageable} {default}
double G1PeriodicGCSystemLoadThreshold = 0.000000 {manageable} {default}
bool HeapDumpAfterFullGC = false {manageable} {default}
bool HeapDumpBeforeFullGC = false {manageable} {default}
int HeapDumpGzipLevel = 0 {manageable} {default}
bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
ccstr HeapDumpPath = {manageable} {default}
uintx MaxHeapFreeRatio = 70 {manageable} {default}
uintx MinHeapFreeRatio = 40 {manageable} {default}
bool PrintClassHistogram = false {manageable} {default}
bool PrintConcurrentLocks = false {manageable} {default}
bool ShowCodeDetailsInExceptionMessages = true {manageable} {default}
size_t SoftMaxHeapSize = 1073741824 {manageable} {ergonomic}
Il y en a quatorze, et six concernent les heap dumps. La dernière colonne dit d’où vient la valeur, par exemple default, command line, ou ergonomic quand la JVM l’a calculée. Après un VM.set_flag, elle indique management.
Changer un flag sans redémarrer
Le service n’a pas été lancé avec -XX:+HeapDumpOnOutOfMemoryError. Si la heap déborde cette nuit, il n’y aura rien à analyser. VM.set_flag corrige cela sur la JVM qui tourne :
$ jcmd 41959 VM.set_flag HeapDumpOnOutOfMemoryError true
41959:
Command executed successfully
$ jcmd 41959 VM.set_flag HeapDumpPath /var/dumps
41959:
Command executed successfully
$ jcmd 41959 VM.flags | tr ' ' '\n' | grep HeapDump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/dumps
Seuls les flags manageable acceptent ce changement. Pour les autres, la réponse est courte :
$ jcmd 41959 VM.set_flag MaxHeapSize 2g
41959:
only 'writeable' flags can be set
La taille de la heap et le choix du GC se fixent au démarrage. Le changement fait avec VM.set_flag ne survit pas à un redémarrage. Notez-le dans le ticket de l’incident, puis ajoutez le flag à la configuration.
Activer un log GC à chaud
On veut voir les pauses GC dans un fichier à part, sans relancer le service. VM.log ajoute une sortie au système de logs de la JVM :
$ jcmd 41959 VM.log output=file=/tmp/gc-live.log what=gc decorators=uptime
41959:
Command executed successfully
$ head -3 /tmp/gc-live.log
[43.331s] GC(43) Pause Young (Normal) (G1 Evacuation Pause) 65M->20M(77M) 0.387ms
[43.636s] GC(44) Pause Young (Normal) (G1 Evacuation Pause) 65M->20M(77M) 0.405ms
[43.926s] GC(45) Pause Young (Normal) (G1 Evacuation Pause) 65M->20M(77M) 0.857ms
Les lignes arrivent tout de suite, dans le format habituel des logs GC. what accepte les mêmes tags que -Xlog. VM.log list affiche la liste complète des tags, puis les sorties en cours, numérotées. Ici, les sorties 0 et 1 sont stdout et stderr, et les sorties 2 et 3 viennent de la ligne de commande. La nouvelle sortie a reçu le numéro 4. Pour l’arrêter :
$ jcmd 41959 VM.log output=#4 what=all=off
41959:
Command executed successfully
Après cette commande, VM.log list ne montre plus la sortie 4. Le fichier reste sur le disque. L’aide de la commande annonce Impact: Low: No impact.
Les threads et les locks
Le symptôme est une latence qui monte. On regarde d’abord les threads. Thread.print donne le même résultat que jstack, et l’option -l ajoute les locks java.util.concurrent :
$ jcmd 41959 Thread.print -l
41959:
2026-10-20 15:33:48
Full thread dump OpenJDK 64-Bit Server VM (25.0.4.1+10-LTS mixed mode, sharing):
...
"invoice-worker-1" #27 [41731] daemon prio=5 os_prio=31 cpu=49682.82ms elapsed=403.22s tid=0x000000093cd79800 nid=41731 runnable [0x00000001720e2000]
java.lang.Thread.State: RUNNABLE
at Billing.lambda$main$1(Billing.java:44)
- locked <0x0000000440000708> (a java.lang.Object)
...
"invoice-worker-2" #28 [41219] daemon prio=5 os_prio=31 cpu=49654.15ms elapsed=403.22s tid=0x000000093cd7a000 nid=41219 waiting for monitor entry [0x00000001722ee000]
java.lang.Thread.State: BLOCKED (on object monitor)
at Billing.lambda$main$1(Billing.java:41)
- waiting to lock <0x0000000440000708> (a java.lang.Object)
...
Un thread tient le lock 0x0000000440000708 et calcule. Les sept autres invoice-worker attendent ce même lock, à l’état BLOCKED. Le champ cpu= confirme ce constat : chacun des huit threads a consommé environ 50 s de CPU en 403 s. À eux huit, ils n’occupent qu’un seul CPU.
Un seul dump ne prouve rien. Prenez-en trois, à quelques secondes d’écart. Si le même lock bloque les mêmes threads à chaque fois, la contention est confirmée.
Les virtual threads en attente n’apparaissent pas dans Thread.print. L’application en a cinquante, et le dump ci-dessus n’en montre aucun. Thread.dump_to_file les inclut, et son format JSON les regroupe par executor :
$ jcmd 41959 Thread.dump_to_file -format=json -overwrite /tmp/threads.json
41959:
Created /tmp/threads.json
$ jq -r '.threadDump.threadContainers[] | "\(.container): \(.threads | length)"' /tmp/threads.json
<root>: 20
java.util.concurrent.ThreadPoolExecutor@6f94fa3e: 0
java.util.concurrent.ThreadPerTaskExecutor@35f021a2: 50
ForkJoinPool.commonPool/jdk.internal.vm.SharedThreadContainer@8dcaabe: 0
ForkJoinPool-1/jdk.internal.vm.SharedThreadContainer@3c157736: 1
Les cinquante virtual threads sont dans le ThreadPerTaskExecutor. Chaque entrée contient "virtual": true, son état et sa stack. Ici, ils sont tous en TIMED_WAITING dans LinkedBlockingQueue.poll. Deux autres commandes, Thread.vthread_scheduler et Thread.vthread_pollers, décrivent le scheduler et les threads de polling réseau :
$ jcmd 41959 Thread.vthread_scheduler
41959:
java.util.concurrent.ForkJoinPool@48f602f3[Running, parallelism = 10, size = 0, active = 0, running = 0, steals = 60, tasks = 0, submissions = 0, delayed = 50]
parallelism = 10 est le nombre maximal de carriers, un par CPU. size = 0 : aucun carrier n’existe en ce moment. delayed = 50 : cinquante attentes avec délai. Ce sont les cinquante poll avec timeout. Pour la lecture détaillée d’un dump qui mélange platform threads et virtual threads, voir les virtual threads en production.
Le contenu de la heap, avec ou sans dump
Trois commandes couvrent la heap. Leur coût augmente de l’une à l’autre.
$ jcmd 41959 GC.heap_info
41959:
garbage-first heap total reserved 1048576K, committed 263168K, used 224410K [0x0000000440000000, 0x0000000480000000)
region size 1024K, 70 young (71680K), 4 survivors (4096K)
La JVM réserve 1 Go, c’est le -Xmx1g. Elle a demandé 257 Mo au système, la valeur committed. Elle en utilise 219 Mo, et ce chiffre inclut les objets morts que le GC n’a pas encore libérés. Cette commande ne provoque aucune pause.
GC.class_histogram compte les objets vivants par classe :
$ jcmd 41959 GC.class_histogram | head -6
41959:
num #instances #bytes class name (module)
-------------------------------------------------------
1: 165364 148930832 [B (java.base@25.0.4.1)
2: 18748 3584704 [Ljdk.internal.vm.FillerElement; (java.base@25.0.4.1)
3: 74953 2398496 java.util.concurrent.ConcurrentHashMap$Node (java.base@25.0.4.1)
On lit 142 Mo de tableaux d’octets et 74 953 nœuds de ConcurrentHashMap. Le nombre de nœuds correspond au cache : un peu moins de 200 entrées par seconde depuis 400 s. Chaque entrée contient un tableau de 2 Ko, ce qui donne l’ordre de grandeur des 142 Mo. On trouve la cause sans heap dump.
Pour ne compter que les objets vivants, la commande déclenche d’abord un Full GC. Le log GC le montre : Pause Full (Heap Inspection Initiated GC) 154M->153M(259M) 7.958ms. L’option -all compte tout, y compris les objets morts, et évite ce Full GC.
Quand l’histogramme ne suffit pas, il faut le dump complet :
$ jcmd 41959 GC.heap_dump -gz=1 /tmp/billing.hprof.gz
41959:
Dumping heap to /tmp/billing.hprof.gz ...
Heap dump file created [8275050 bytes in 0.098 secs]
Le fichier compressé fait 7,9 Mo pour 153 Mo d’objets vivants. -gz=1 compresse pendant l’écriture, ce qui compte quand le disque du conteneur est petit. Dans un conteneur, écrivez le fichier dans /tmp, puis récupérez-le avec docker cp ou kubectl cp. La lecture du fichier est le sujet de diagnostiquer une fuite mémoire avec un heap dump.
Les commandes du tableau ci-dessous arrêtent l’application pendant leur exécution. Les durées viennent de -Xlog:safepoint, sur cette heap de 219 Mo utilisés et 153 Mo vivants, avec 42 threads dans le dump :
| Commande | Pause de l’application | Remarque |
|---|---|---|
Thread.print -l | 17,7 ms | |
GC.class_histogram | 20,2 ms | dont un Full GC de 8,0 ms |
GC.heap_dump -gz=1 | 96,5 ms | dont un Full GC de 8,6 ms, fichier de 7,9 Mo |
GC.heap_dump -all -gz=1 | 110,7 ms | pas de Full GC, fichier de 10,7 Mo |
Sur une heap de plusieurs Go, le dump prend plusieurs secondes, et tous les threads sont arrêtés pendant ce temps. Prenez-le sur une instance sortie du load balancer quand c’est possible.
GC.run force un Full GC. Sur la même JVM, plus tôt, à 202 s de fonctionnement : Pause Full (Diagnostic Command) 97M->77M(132M) 7.624ms. C’est le moyen de savoir si la mémoire utilisée est vivante ou morte, avant de parler de fuite.
La mémoire native
Le RSS du conteneur dépasse la heap. VM.native_memory détaille la mémoire native par usage, à une condition :
$ jcmd 41959 VM.native_memory summary
41959:
Native memory tracking is not enabled
Le suivi doit être activé au démarrage, avec -XX:NativeMemoryTracking=summary. Sans ce flag, la commande ne peut rien dire. Sur une JVM lancée avec le flag, la sortie commence ainsi :
$ jcmd 42500 VM.native_memory summary
42500:
Native Memory Tracking:
(Omitting categories weighting less than 1KB)
Total: reserved=2662507KB, committed=413403KB
malloc: 88043KB #12010, peak=91207KB #12037
mmap: reserved=2574464KB, committed=325360KB
- Java Heap (reserved=1048576KB, committed=264192KB)
(mmap: reserved=1048576KB, committed=264192KB, at peak)
- Class (reserved=1048674KB, committed=226KB)
(classes #901)
...
- Thread (reserved=92303KB, committed=719KB)
(threads #45)
(stack: reserved=92160KB, committed=576KB, peak=576KB)
...
- Code (reserved=249828KB, committed=7812KB)
...
- GC (reserved=72515KB, committed=57219KB)
La colonne committed est celle qui compte. Ici, la heap prend 258 Mo, le GC 56 Mo pour ses structures, le code compilé 8 Mo. Les 45 threads réservent 90 Mo de stacks mais n’en utilisent que 576 Ko.
Pour voir ce qui grossit, prenez un point de référence, attendez, puis demandez la différence :
$ jcmd 42500 VM.native_memory baseline
42500:
Baseline taken
$ jcmd 42500 VM.native_memory summary.diff
42500:
...
Total: reserved=2642000KB -20506KB, committed=413456KB +54KB
...
- Thread (reserved=71789KB -20515KB, committed=765KB +45KB)
(threads #35 -10)
Chaque ligne donne l’écart depuis le point de référence. Ici, dix threads se sont arrêtés et ont rendu 20 Mo de mémoire réservée. Une catégorie qui monte à chaque mesure désigne le suspect. L’analyse complète est dans la mémoire hors-heap.
JFR sans copier de fichier
JFR se pilote entièrement avec jcmd. On démarre un enregistrement, on vérifie qu’il tourne, et on le lit sur place :
$ jcmd 41959 JFR.start name=oncall settings=profile filename=/tmp/oncall.jfr
41959:
Started recording 1. No limit specified, using maxsize=250MB as default.
Use jcmd 41959 JFR.dump name=oncall to copy recording data to file.
$ jcmd 41959 JFR.check
41959:
Recording 1: name=oncall maxsize=250.0MB (running)
JFR.view lit l’enregistrement en cours sans écrire de fichier. Après vingt secondes :
$ jcmd 41959 JFR.view hot-methods
41959:
Java Methods that Execute the Most
Method Samples Percent
-------------------------------------------------------------------------- ------- -------
Billing.lambda$main$1() 1,739 94.05%
java.util.concurrent.locks.LockSupport.parkNanos(long) 97 5.25%
Billing.lambda$main$2() 4 0.22%
...
$ jcmd 41959 JFR.view contention-by-site
41959:
Contention by Site
StackTrace Count Avg. Max.
----------------------------------------------------------- ----- ------ ------
Billing.lambda$main$1() 1,015 139 ms 143 ms
La vue hot-methods montre où passe le CPU : dans lambda$main$1, la boucle des invoice-worker. La vue contention-by-site donne ce que le thread dump ne donnait pas. En vingt secondes, les threads ont attendu ce lock 1 015 fois, 139 ms en moyenne. JFR.view sans argument liste 79 vues, par exemple gc, allocation-by-class, exception-by-site ou pinned-threads.
Quand il faut aller plus loin dans l’analyse, on écrit le fichier et on arrête l’enregistrement :
$ jcmd 41959 JFR.dump name=oncall filename=/tmp/oncall-dump.jfr
41959:
Dumped recording "oncall", 960.4 kB written to:
/tmp/oncall-dump.jfr
$ jcmd 41959 JFR.stop name=oncall
41959:
Stopped recording "oncall", 1.3 MB written to:
/tmp/oncall.jfr
JFR.dump copie l’enregistrement sans l’arrêter. JFR.stop l’arrête et écrit le fichier donné au départ.
Charger async-profiler sans redémarrer
async-profiler a sa propre commande, asprof. Quand seule la bibliothèque est présente, JVMTI.agent_load la charge dans la JVM qui tourne. La chaîne d’options contient des =. L’analyseur de commandes de la JVM la coupe au premier =, comme une option clé=valeur. Sans apostrophes, le profiler reçoit start,event, sans la valeur :
$ jcmd 41959 JVMTI.agent_load /opt/async-profiler/lib/libasyncProfiler.dylib start,event=cpu,file=/tmp/profile.html
41959:
return code: 100
La JVM écrit [ERROR] event must not be empty sur sa sortie standard. Des apostrophes autour de la chaîne résolvent le problème :
$ jcmd 41959 JVMTI.agent_load /opt/async-profiler/lib/libasyncProfiler.dylib "'start,event=cpu,file=/tmp/profile.html'"
41959:
return code: 0
$ jcmd 41959 JVMTI.agent_load /opt/async-profiler/lib/libasyncProfiler.dylib "'stop,file=/tmp/profile.html'"
41959:
return code: 0
Le code de retour 0 vient de l’agent. La JVM écrit Profiling started sur sa sortie standard, et le flamegraph est dans /tmp/profile.html après le stop. Le test a été fait avec async-profiler 4.1.
Dans un conteneur
Trois choses changent quand la JVM tourne dans un conteneur.
La première est l’utilisateur. jcmd doit tourner avec le même utilisateur que la JVM. Au premier attach, la JVM crée un socket /tmp/.java_pid<pid> pour recevoir les commandes. jcmd refuse de l’utiliser s’il appartient à un autre utilisateur :
$ docker exec --user 1001:1001 billing jcmd 1 VM.version
1:
java.io.IOException: well-known file /tmp/.java_pid1 is not secure: file should be owned by the current user (which is 1001) but is owned by 1000
Avec docker exec --user 1000:1000, ou kubectl exec dans un pod qui déclare le bon runAsUser, la commande passe. root peut toujours se connecter à toutes les JVM.
La deuxième est l’image. eclipse-temurin:25-jre ne contient pas jcmd :
$ docker run --rm eclipse-temurin:25-jre ls /opt/java/openjdk/bin
java
jfr
jrunscript
jwebserver
keytool
rmiregistry
Avec une image JRE, aucune des commandes de cet article n’est disponible dans le conteneur. Si vous choisissez une image JRE pour sa taille, décidez en même temps comment vous diagnostiquerez un incident.
La troisième est le flag -XX:+DisableAttachMechanism. Certaines équipes l’ajoutent pour la sécurité. Avec lui, jcmd répond AttachNotSupportedException: The VM does not support the attach mechanism, et jcmd ne peut rien faire sans redémarrer la JVM. Le flag -XX:-UsePerfData retire seulement la JVM de la liste affichée par jcmd. Avec le pid, les commandes passent.
Sur Linux, deux des trois commandes ajoutées servent ici. System.trim_native_heap demande à la libc de rendre au système la mémoire native libérée :
$ docker exec billing jcmd 1 System.trim_native_heap
1:
Trim native heap: RSS+Swap: 147M->144M (-3176K)
Sur cette petite application, le gain est de 3 Mo. Quand le RSS d’une JVM monte et que ni la heap ni VM.native_memory ne l’expliquent, c’est un test rapide. Compiler.perfmap écrit /tmp/perf-<pid>.map, le fichier qui permet à perf de nommer les méthodes Java compilées.
Les autres commandes qui servent
| Commande | Ce qu’elle donne |
|---|---|
VM.info | Un rapport proche d’un fichier hs_err, sans crash : version, ligne de commande, machine, heap, threads, bibliothèques, variables d’environnement. En conteneur, une section container (cgroup) information avec les limites vues par la JVM. |
VM.system_properties | Toutes les propriétés système, dont file.encoding. |
VM.metaspace | L’usage total du Metaspace. Ici 35 loaders, 2214 classes (1361 shared). |
VM.classloader_stats | Une ligne par chargeur, avec son nombre de classes. Un nombre de chargeurs qui monte sans arrêt signale une fuite de classes. |
Compiler.codecache | L’occupation du code cache : used=3884Kb sur size=245776Kb, et Compilation: enabled. |
VM.events | Les derniers événements internes de la JVM : compilations, désoptimisations, historique de la heap. |
ManagementAgent.start_local | Démarre l’agent JMX local sans redémarrer, pour brancher un outil graphique sur la machine. |
GC.finalizer_info | Le nombre d’objets en attente de finalisation. |
En résumé
jcmd est dans chaque JDK. Il parle à la JVM qui tourne, sans redémarrage et sans agent. Sans argument, il liste les JVM. Avec help, il liste ses commandes, et chaque commande annonce son impact.
VM.command_line, VM.flags et VM.set_flag montrent la configuration réelle et permettent de corriger un flag manageable à chaud, comme HeapDumpOnOutOfMemoryError. VM.log active un log GC sans redémarrer.
Thread.print donne le thread dump, et Thread.dump_to_file -format=json ajoute les virtual threads. GC.heap_info, GC.class_histogram et GC.heap_dump couvrent la heap. GC.heap_info ne provoque aucune pause. Les deux autres arrêtent l’application de 20 ms à 111 ms sur une heap de 219 Mo. GC.class_histogram et GC.heap_dump déclenchent un Full GC, sauf avec -all.
VM.native_memory demande -XX:NativeMemoryTracking au démarrage. JFR.start, JFR.view et JFR.dump pilotent JFR, et JFR.view lit l’enregistrement sur place. JVMTI.agent_load charge async-profiler, avec des apostrophes autour des options.
En conteneur, il faut le même utilisateur que la JVM, une image JDK et pas de -XX:+DisableAttachMechanism.
Et après ?
Pour lire en détail ce que Thread.print affiche, voir lire un thread dump avec jstack. Pour aller plus loin avec JFR.view et les enregistrements continus, voir JFR en production.