Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Outils de monitoring (mémoire/GC)

Options du GC Hotspot La documentation via Javadoc



Accès rapide :
   Utilisation de la JConsole
   Visual GC : Visual Garbage Collection Monitoring Tool
   GCViewer
   JMeter et les tests de montée en charge
   Java Visual VM
   Memory Analyzer Tool

Vouloir chasser les fuites mémoires ou chercher à vérifier s'il y a des fuites dans une application quelconque (application web ou non) est un objectif louable. Néanmoins, il nous faut être en mesure de collecter certains indicateurs : taux d'occupation des différents espaces mémoire, empreinte du garbage collector, ... Il nous faut donc utiliser un certain nombre d'outils.

L'écosystème Java a produit un très grand nombre d'outils pouvant être utilisés pour profiler une JVM. Certains d'entre eux sont directement proposés par le Java SE (sous condition d'avoir le JDK(Java Development Kit : une distribution de Java SE intégrant les outils de développement) ) et d'autres sont à télécharger sur Internet. Certains sont gratuits et d'autres sont payants. Dans ce cours/tutoriel nous ne présenterons que des outils gratuits, même si certains autres outils sont extrêmement intéressants (je pense par exemple à JProfiler).

Utilisation de la JConsole

En premier lieu, je voulais vous présenter la JConsole : elle est proposée de base dans le JDK. Elle se connecte à un processus Java en cours d'exécution (avec possibilité de connexion à distance). A la base, il s'agit du client universel JMX (Java Management eXtension) : elle permet donc d'accéder aux MBeans (Managed Beans) de votre JVM pour administrer cette JVM. La capture d'écran suivante montre une JConsole connectée à un serveur Tomcat (pour information, Catalina est le nom originel de Tomcat ; aujourd'hui il est associé au conteneur de servlets contenu dans Tomcat). Notez que la capture proposée correspond à l'onglet "MBeans".

Note : vous retrouverez, dans la capture d'écran ci-dessus, l'acronyme PS : celui-ci signifie Parallel Scanveger (nettoyeur parallèle). Cet acronyme apparait si votre JVM utilise plusieurs threads pour l'exécution du GC. Il est tout à fait possible que vous ne l'ayez pas sur votre propre console.

Notez qu'on y retrouve les informations sur les différentes zones mémoires (Code Cache, PS Perm Gen, PS Eden Space, PS Suvivor Space, PS Old Gen) ainsi que des informations sur les deux types de GC (PS MarkSweep (Full GC) et PS Scavenge (Minor GC)). Cliquez sur les sections "Attributes" pour de plus amples informations. Le problème de cet affichage c'est que nous avons les valeurs instantanées, mais pas l'historique des valeurs (notez cependant l'obligation de cliquer sur le bouton "Refresh" pour réactualiser les données affichées). Pour obtenir l'historique des valeurs, préférez l'utilisation de l'onglet "Memory". Des courbes vous seront proposées pour chaque espace mémoire. Voici un exemple d'affichage possible : notez bien que l'historique des données démarre lors de l'accrochage de la JConsole à votre processus.

Pour changer l'espace mémoire pour lequel produire une courbe d'historique, il vous suffit de cliquer sur l'une des jauges vertes en bas à droite de la fenêtre. En laissant la souris immobile suffisamment longtemps sur une jauge, vous verrez apparaître une bulle d'information qui vous indiquera le nom de l'espace mémoire associé. Les blocs "Heap" et "Non-Heap" correspondent respectivement à la somme des blocs mémoire nettoyables par GC et à la somme des blocs de mémoire non nettoyable par GC (Perm Gen et Code cache).

Notez qu'il vous est possible de laisser tourner la JConsole très longtemps, des ré-échantillonnages des valeurs acquises étant régulièrement réalisés afin de garantir de la JConsole ne tombera pas en OutOfMemoryError. Pour ma part, j'ai déjà fait tourner une JConsole pendant plus de trois semaines afin de vérifier qu'il n'y ait pas de fuites de mémoire. En parallèle j'utilise l'outil JMeter pour simuler des utilisateurs. Si au final d'une campagne de tests sur plusieurs jours, la consommation moyenne de la mémoire est en croissance constante, alors il y a de fortes chances qu'il y ait une ou plusieurs fuites mémoire.

IMPORTANT : à partir de la version 8.0 du standard Java SE, Hostpot supprime le PERM.

Visual GC : Visual Garbage Collection Monitoring Tool

Cet outil est proposé par Oracle, néanmoins il n'est pas livré dans le JDK : c'est dommage. Vous pourrez le trouver en recherchant "VisualGC" dans votre moteur de recherche favori. Cet outil permet de se connecter à une JVM en cours d'exécution et de faire un état de l'activité du GC et de la consommation de la mémoire. Cet outil est aussi connu sous le nom de JvmStat. D'une certaine manière, il est plus précis que la JConsole (notamment, il affiche séparément la consommation de chaque Survivor Space) par contre l'outil n'historise pas les résultats.

Du coup, il y a au moins un point sur lequel il est très pratique : si, lors d'un GC mineur, il y a trop d'instances survivantes par rapport à l'espace disponible dans l'un des deux Survivor Spaces, alors le surplus d'objets sera directement renvoyé dans le Old Space. Néanmoins, les objets déplacés avaient peut-être une espérance de vie courte : cela augmente donc la fréquence de déclenchement des GC majeurs. Dans ce cas, il est peut être nécessaire de modifier le paramètre -XX:SurvivorRatio pour mécaniquement obtenir une augmentation des performances. Visual GC est donc très pratique pour vérifier ce genre de scénario.

Visual GC se lance en mode ligne de commande. Il accepte en paramètre le PID (Process Identifier) de la JVM à profiler. Vous pouvez trouver ce PID via le gestionnaire des tâches de votre système d'exploitation. Néanmoins, il est aussi possible de connaitre les PID de tous les processus Java en cours d'exécution. Pour ce faire, utilisez la commande jps. Cette commande est fournie par le JDK.

Une fois VisualGC lancé, vous obtenez deux (ou trois, précédemment) fenêtres qui affichent des informations sur votre JVM. Notez que l'outil cherche, plus ou moins, à respecter les proportions de tailles entre les différents espaces mémoire.

Si vous regardez bien la fenêtre de gauche, chaque espace mémoire de la JVM vous est présenté (excepté la zone mémoire relative au code). Un jeu de trois couleurs est utilisé. Le quadrillage gris montre l'espace total que votre processus Java pourrait atteindre (dans les limites de -Xmx et -XX:MaxPermSize). Le quadrillage vert montre l'espace de mémoire actuellement engagé par votre processus pour la zone de mémoire considérée. Le bloc orange montre la mémoire utilisée par la JVM dans l'espace alloué : noté que quand la zone orange rattrape le quadrillage vert, alors une nouvelle collecte de mémoire est lancée. Notez aussi les bascules d'utilisation entre les deux "Survivor Spaces".

IMPORTANT : à partir de la version 8.0 du standard Java SE, Hostpot supprime le PERM.

GCViewer

GCViewer est autre outil que l'on peut trouver sur Internet. Il est complémentaire aux deux précédents outils et fonctionne légèrement différemment. Effectivement, GCViewer travaille sur un rapport d'activité de GC et non sur un processus en cours d'exécution. Il est donc nécessaire d'éditer les scripts de lancement de votre serveur (ou tout autre processus Java que vous souhaiteriez analyser). Je vous propose l'ajout des deux options suivantes : -Xloggc:filename et -XX:+PrintGCDetails. Faites bien attention à la localisation du fichier que vous allez produire.

Il est important de noter que l'utilisation de ces deux options ne coute pas beaucoup plus cher en termes de temps d'exécution. Vous pouvez donc générer des rapports d'activité de votre JVM même sur un serveur en production.

Une fois que vous avez produit votre rapport d'activité du GC pour votre JVM vous pouvez lancer GCViewer est ouvrir votre rapport. Pour ce faire lancer la commande suivante dans une console : java -jar gcvierwer-1.29.jar (attention, le numéro de version peut varier). Ouvrez le fichier le log à partir du menu "File". Un rapport visuel apparaît avec plusieurs courbes (de différentes couleurs) : vous avez le choix d'afficher ou non chacune de ces courbes. Notez aussi que certains indicateurs ont été calculés : ils sont présentés en bas à droite de la fenêtre.

L'exemple proposé est typique de ce qu'il ne faut pas qu'il vous arrive. L'application en test consomme de plus en plus d'objets (imaginons une fuite de mémoire). Or la mémoire disponible est trop juste. Ce qu'il faut regarder, c'est la courbe bleue. Elle correspond à la mémoire consommée : on voit bien qu'elle tend vers une limite. Au démarrage, des phases de collectes mémoire ont lieu et la mémoire consommée retombe. Mais vers le dernier quart du diagramme, la mémoire ne redescend quasiment plus : c'est normal car tous les objets sont référencés en mémoire et sont donc considérés comme utiles par la JVM. Du coup, les phases de collecte majeures sont deux plus en plus rapprochées. Un trait noir correspond à un GC majeur : notez, sur la fin le rapprochement des barres. La JVM passe donc un maximum de temps à nettoyer la mémoire. Il faut aussi remarquer que les GC majeurs durent à peu près une seconde. En gros, c'est la catastrophe absolue.

Parmi la liste des indicateurs proposés à droite, j'attire votre attention sur le Throughput (le débit) : il est de 57.08%. Cet indicateur correspond au temps total passé dans vos threads et hors GC par rapport à la durée d'exécution de votre processus. Ici, on passe donc autant de temps dans le GC que dans le programme : ce n'est vraiment pas bon. Un bon débit devrait être de plus de 95%. Un très bon débit serait proche de 99% (seulement 1% de temps consommé par le GC).

La situation ici observée est très proche de ce qui se passe sur un Tomcat hébergeant des applications produisant des fuites mémoires. Au début de son exécution, tout va bien. Puis, progressivement, la mémoire utilisée se rapproche de la limite utilisable. Les GC majeurs deviennent de plus en plus proches et du coup les temps de réponse du serveur se dégradent de plus en plus, jusqu'à ne même plus arriver à avoir de retour de Tomcat étant donné qu'il passe son temps à faire des GCs. Vous allez alors le redémarrer jusqu'à la prochaine fois ou la mémoire sera saturée. Il nous faut donc d'autres outils pour trouver les objets qui ne sont pas relâchés de la mémoire (nous y reviendrons plus loin).

JMeter et les tests de montée en charge

Le mieux est de se rendre compte des problèmes présentés ci-dessus avant de passer l'application en production. Pour ce faire, il vous faut bâtir des batteries de tests de montées en charge (voir des tests d'endurance). JMeter, un outil proposé par la fondation Apache, peut vous permettre de réaliser ces batteries de tests.

JMeter peut être utilisé pour enregistrer un scénario de navigation sur votre serveur Tomcat (ou autre). Pour ce faire, il vous faut configurer JMeter comme étant un proxy HTTP au sein de votre navigateur Web. Ainsi, quand vous allez produire des requêtes HTTP via votre navigateur, JMeter va les réceptionner et les mémoriser. Il va aussi les rediriger sur votre serveur et du coup il recevra les réponses HTTP qu'il mémorisera aussi avant de les retourner au navigateur. Une fois votre scénario totalement enregistré, vous pouvez fermer le navigateur. Il vous est maintenant possible de rejouer votre scénario autant de fois que vous le souhaitez et en parallèle, histoire de simuler plusieurs utilisateurs.

Du coup, le serveur va monter en charge et traiter un grand nombre de requêtes. Son GC va certainement lui aussi commencer à travailler. Si en même temps, vous avez connecté la JConsole sur votre serveur et si vous produisez des logs sur l'activité du GC, il vous sera alors possible de connaitre le comportement de votre serveur. Un conseil : déterminez si votre Throughput est bon ou pas.

Vous pouvez télécharger JMeter sur le serveur Apache à l'adresse suivante : http://jmeter.apache.org.

Java Visual VM

Nous partons maintenant du principe que nous avons effectivement un problème de fuites mémoire. La question est donc de savoir comment déterminer quelles sont les instances qui ne sont pas relâchées et par conséquent avoir des pistes sur les portions de code à corriger. Java Visual VM, un autre outil du JDK peut vous permettre de trouver ces informations.

Java Visual VM est un profileur de code Java : il se connecte sur un processus Java en cours d'exécution de manière très similaire à la JConsole. Pour ce faire, lancer la commande jvisualvm dans votre console. Une fois connectée, la console vous propose différents outils (par le biais de différents onglets). Le premier outil que je vous propose est disponible à partir de l'onglet "Profiler". Deux types de profilage peuvent être effectués : vous pouvez choisir une campagne de profilage de type Memory. Un tableau (réactualisé périodiquement) vous propose une liste de classes ordonnées par tailles utilisées pour chaque type. Notez que vous pouvez filtrer les classes présentées par le biais de l'outil "Class Name filter" présent en bas de la fenêtre. Vous pourrez ainsi vérifier la cohérence des volumétries d'instances constatées.

Un autre outil est très pratique : il est accessible à partir de l'onglet "Monitor". Cet onglet affiche des informations très proches de celles proposées par la JConsole. Notez néanmoins la présence du bouton "Heap Dump" : il produira un fichier contenant un dump de l'ensemble des objets actuellement en mémoire.

Une fois le fichier de dump produit, vous pouvez l'enregistrer sur disque (bouton droit de la souris sur le dump) et vous pourrez le consulter au travers de différents type de rapports. La capture d'écran ci-dessous présente le rapport de type "Classes".

Notez que cet outil est basé sur une coquille pouvant contenir des plugins (en fait, il s'agit d'une partie du profileur de la plate-forme NetBeans). Les onglets présentés dans les captures d'écran de l'outil sont associés à ces plugins et il vous est possible de l'enrichir en ligne avec de nouveaux outils de profilage. Par exemple, il est possible de déployer une version de VisualGC directement dans JVisualVM. Testez ces possibilités par curiosité.

Memory Analyzer Tool

Pour clore ce chapitre sur les outils standard de profilage d'application Java, je ne pouvais pas passer à côté de celui qui m'a sauvé x fois la vie : Memory Analizer Tool (MAT pour les intimes). Cet outil est un plugin pour l'IDE Eclipse. Il est téléchargeable à partir du site officiel Eclipse. Il est aussi possible de l'installer en ligne à partir de l'IDE. Cet outil travaille sur un fichier de dump mémoire d'une JVM.

Je vous propose trois solutions pour obtenir un fichier de dump mémoire d'une JVM (deux d'entre elles ont déjà étaient vues):

Ensuite, démarrer votre atelier Eclipse. Si vous avez un doute, vous pouvez vérifier si MAT est installé en sélectionnant l'élément de menu "About Eclipse" dans le menu "Help". Une boîte de dialogue apparait : en cliquant sur le bouton "Installation Details" vous pourrez vérifier la présence de MAT. Si celui-ci n'est pas installé, en cliquant sur "Install new software..." dans le menu "Help", vous pourrez procéder à son installation.

Bien qu'une perspective Eclipse soit dédiée à l'exploitation de MAT, il vous est possible de simplement cliquer sur un fichier de rapport (il faut qu'il soit présent dans le projet Eclipse considéré et il faut que le fichier ait l'extension .hprof). Une fois l'analyse du fichier lancée, une première boite de dialogue vous demande quel type d'analyse vous souhaitez faire : si vous chassez les fuites mémoire, demandez un rapport de type "Leak Suspects Report".

Ensuite l'outil le plus important (du moins, à mon sens) est le "Dominator Tree". Il présente les classes Java retenant le plus d'octets (directement et indirectement) en mémoire. Voici un exemple d'affichage de type "Dominator Tree" : pour l'obtenir, veuillez cliquer sur l'icône juste au-dessus du pointeur de la souris de la capture d'écran.

Quelques explications s'imposent : deux notions de tailles sont présentées pour chaque classe. La taille "Peu profonde" (Shallow Heap en anglais) : elle correspond à la taille consommée par les instances, sans considérer les tailles des zones pointées (si certains attributs sont des pointeurs). La taille "Retenue" (Retained Heap) qui correspond à l'ensemble des octets retenus (directement ou indirectement, au travers des zones pointées) par vos objets. Bien entendu, la taille "Retenue" et plus grande que la taille "Peu profonde". Les classes étant triées par taille décroissante, cela vous permet de facilement localiser les grosses fuites de mémoire.

Pour comprendre comment sont structurés les octets en profondeur, il vous suffit de déployer l'arborescence de la vue "Dominator Tree". Il existe beaucoup d'autres possibilités associées à MAT : je vous laisserai les découvrir de part vous-même.

Options du GC Hotspot La documentation via Javadoc