Dans cette série, on a déjà utilisé une demi-douzaine d’outils : les logs du GC, jstat, plusieurs commandes jcmd, Eclipse MAT. Chacun répond à une question précise, et chacun a un coût. Cet article les range, puis montre JFR, l’outil le plus complet du JDK, sur un exemple : trouver la méthode qui crée des millions d’objets.

Quel outil pour quelle question

QuestionOutilCommandeCoût
Le GC fait-il des pauses longues ? Le heap après GC monte-t-il ?Logs du GC-Xlog:gc*:file=gc.logà garder en production
Quelle zone se remplit, en direct ?jstatjstat -gcutil <pid> 1snon indiqué ; « expérimental et non supporté » d’après sa doc
Combien de heap est utilisé ?jcmdjcmd <pid> GC.heap_infomoyen
Quelle classe grossit ?jcmdjcmd <pid> GC.class_histogramélevé : lance un Full GC
Qui retient les objets ?heap dump + Eclipse MATjcmd <pid> GC.heap_dumpélevé : un gros fichier
Le Metaspace grossit-il ?jcmdjcmd <pid> VM.metaspacemoyen
Que fait la mémoire native ?jcmd + NMT-XX:NativeMemoryTracking=summary, puis jcmd <pid> VM.native_memorymoyen
Que font les threads ?jcmdjcmd <pid> Thread.printmoyen
Quelle méthode alloue, où sont les pauses, dans le temps ?JFR-XX:StartFlightRecording, puis jfr viewfaible avec la configuration default

Le coût « moyen » ou « élevé » des commandes jcmd est celui qu’indique leur documentation. Chacun de ces outils a déjà servi dans un article : les logs du GC, les OutOfMemoryError, les fuites mémoire et le heap dump avec Eclipse MAT.

JFR pas à pas

Java Flight Recorder (JFR) est inclus dans le JDK. Il enregistre des événements de la JVM et de l’application : GC, allocations, threads, méthodes les plus utilisées…

Enregistrer. Au démarrage :

java -XX:StartFlightRecording:filename=hot.jfr,settings=profile -jar app.jar

Ou sur une application déjà lancée : jcmd <pid> JFR.start, puis jcmd <pid> JFR.dump filename=hot.jfr pour écrire l’enregistrement.

Choisir la configuration. D’après la documentation d’Oracle, la configuration default est recommandée pour les enregistrements continus, avec en général moins de 1 % de surcoût. La configuration profile enregistre plus d’événements, comme les échantillons d’allocation : elle sert pour analyser un problème. La doc ne donne pas de chiffre pour son surcoût.

Lire. La commande jfr du JDK affiche des tableaux, sans interface graphique :

jfr view allocation-by-site hot.jfr
jfr view gc hot.jfr

Avant Java 21 : jfr view n’existe pas. En Java 17, la commande répond jfr: unknown command 'view' : il faut passer par jfr print ou par JDK Mission Control.

Exemple : la méthode qui crée des millions d’objets

HotAllocation.java construit 10 millions de libellés avec String.format. On l’enregistre avec JFR, puis :

jfr view allocation-by-site hot.jfr

Method                                              Allocation Pressure
--------------------------------------------------- -------------------
java.util.Formatter.parse(String)                                24.70%
java.lang.Long.toString(long)                                    18.83%
java.lang.AbstractStringBuilder.<init>(int)                      14.20%
java.util.Arrays.copyOfRange(byte[], int, int)                   10.61%
java.util.ArrayList.grow(int)                                    10.35%

Formatter.parse crée le plus d’objets. Ce sont des échantillons : les pourcentages changent un peu d’un lancement à l’autre, mais le haut du tableau reste le même. Reste à trouver l’appelant dans notre code : la pile d’un échantillon le donne.

jfr print --events jdk.ObjectAllocationSample --stack-depth 6 hot.jfr

  stackTrace = [
    java.util.Formatter.parse(String) line: 2810
    java.util.Formatter.format(Locale, String, Object[]) line: 2744
    java.util.Formatter.format(String, Object[]) line: 2698
    java.lang.String.format(String, Object[]) line: 4455
    HotAllocation.label(int) line: 16
    HotAllocation.main(String[]) line: 24

Le coupable : String.format, appelé dans label(). Chaque appel analyse le motif "order-%08d" et crée plusieurs objets. Dans FixedAllocation.java, les mêmes libellés sont construits avec un StringBuilder. Même résultat, avec -Xmx256m et JFR actif :

HotAllocationFixedAllocation
Java 2532 GC, 2,7 s9 GC, 0,7 s
Java 2144 GC12 GC
Java 1794 GC10 GC

Moins d’objets créés, donc moins de GC : c’est le « débit d’allocation » de l’article sur les logs du GC.

jcmd, le couteau suisse

jcmd envoie une commande à une JVM qui tourne. jcmd <pid> help liste toutes les commandes disponibles. Celles de la série : GC.heap_info, GC.class_histogram, GC.heap_dump, GC.run, VM.metaspace, VM.native_memory, Thread.print, et JFR.start / JFR.dump. Sa documentation indique l’impact de chacune : à lire avant de lancer une commande « High » sur une production chargée.

VisualVM et JDK Mission Control

Pour ceux qui préfèrent une fenêtre :

  • VisualVM suit une JVM en direct (CPU, activité du GC, heap, Metaspace), affiche ses threads, prend et ouvre les heap dumps, et fait du profilage simple.
  • JDK Mission Control ouvre les fichiers .jfr et montre les mêmes données que jfr view, en graphiques.

Le même hot.jfr, ouvert dans JMC. JMC compte les allocations par classe, jfr view allocation-by-site par méthode : les deux vues viennent du même enregistrement.

JDK Mission Control, page Memory : en haut, les allocations par classe (byte[] 50,6 %, Object[] 18,9 %, java.util.Formatter$FixedString 14,1 %) ; en bas, le heap en dents de scie, avec une ligne verticale par GC.

Avant Java 9 : VisualVM était livré avec le JDK (jvisualvm). Il a été retiré en Java 9 et se télécharge à part.

Les deux s’installent séparément. Les commandes de cet article, elles, sont déjà dans le JDK et marchent sur un serveur sans écran.

En production

  • À garder en continu : les logs du GC dans un fichier, et JFR avec la configuration default.
  • Ponctuellement, en connaissant le coût : GC.class_histogram (un Full GC), GC.heap_dump (un gros fichier, avec des données sensibles), JFR avec profile.
  • Avant d’agir : relire la ligne « Impact » de la commande dans la documentation de jcmd.

Testé avec Java 17, 21 et 25 (Temurin 17.0.20, 21.0.12, 25.0.4), sur macOS ; les programmes compilent avec Java 8.

Sources : la documentation de jcmd, jfr et jstat, le guide de Flight Recorder sur ses configurations (JDK 25), JDK-8306703 (jfr view) et JDK-8180002 (VisualVM retiré du JDK).

Pour aller plus loin