Analyser la mémoire d’une application Java
Tout développeur, un jour ou l’autre, est confronté à un problème de mémoire. Java permet de ne pas se soucier de la gestion de la mémoire, mais comme toute chose automatique, c’est bien quand ça marche, et c’est moins bien quand on rencontre des problèmes !
Heureusement, les derniers JDK proposent des outils plus ou moins ergonomiques afin de surveiller et mieux comprendre ce qu’il se passe. Avant toute chose, voici un (très) bref rappel du fonctionnement de la gestion de la mémoire Java et de son « garbage collector » (ou GC).
Java et le « garbage collector »
Afin d’optimiser le fonctionnement du GC, Java utilise par défaut un système mettant en oeuvre plusieurs zones mémoire. Ces zones sont utilisées par les objets plus ou moins vieux. Je ne rentrerai pas dans les détails de ces zones ni comment les dimensionner. Le GC utilise 3 zones au fur et à mesure que les objets vieillisent :
- Survivor space : Il est en fait séparé en 2. Chaque zone est active alternativement. Les objets nouvellement créés sont créés dans la zone active. Au moment du passage du YGC (Young Garbage Collector), celui-ci va nettoyer le Survivor Space en recopiant les objets encore référencés dans la seconde zone qui devient active à son tour. Au bout d’un certain nombre d’allez-retour, les objets sont alors transférés dans le « Eden space ».
- Young Space : Comme nous venons de le voir cette zone reçoit les objets encore jeunes qui ont survécus à plusieurs YGC.
- Old Space : Lorsque la place vient à manquer dans le Young Space, un Full Garbage Collector (FGC) est lancé et les objets les plus anciens de la Young Zone sont transférés dans cette zone.
En plus de ces 3 zones, il existe le Permanent Space qui comme son nom l’indique est utilisé pour le cache du code et les objets statiques.
Lorsqu’un problème de mémoire arrive, c’est en général car des objets restent référencés (par une liste ou autre) et finissent donc par remplir le Old Space. Un des premiers outils bien pratique pour suivre ces mouvements de mémoire entre les zones est jstat qui est livré avec le JDK. Différentes options existent, mais la surveillance du Garbage Collector est sans doute le plus intéressant dans notre cas.
jstat -gc -h10 <pid> 5s
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 5376.0 10944.0 5319.5 0.0 723712.0 393938.3 571776.0 548046.1 61568.0 58080.5 754 27.767 26 16.290 44.057 9600.0 6976.0 0.0 6964.5 691648.0 232030.5 571776.0 550188.2 61568.0 58084.8 755 27.801 26 16.290 44.091 5504.0 10560.0 5465.1 0.0 777088.0 106216.4 571776.0 553407.3 61568.0 58085.2 756 27.870 26 16.290 44.161 5504.0 10560.0 5465.1 0.0 777088.0 608730.8 571776.0 553407.3 61568.0 58085.7 756 27.870 26 16.290 44.161 9856.0 5824.0 0.0 5801.2 865984.0 263527.0 571776.0 555880.7 61568.0 58086.1 757 27.892 26 16.290 44.182 9856.0 5824.0 0.0 5801.2 865984.0 696381.0 571776.0 555880.7 61568.0 58088.1 757 27.892 26 16.290 44.182 8192.0 10496.0 8180.9 0.0 826944.0 359223.6 571776.0 558621.8 61568.0 58088.4 758 27.910 26 16.290 44.201 7552.0 5312.0 0.0 5296.1 789824.0 71320.0 571776.0 563405.1 61568.0 58088.9 759 27.963 26 16.290 44.253 7552.0 5312.0 0.0 5296.1 789824.0 648105.7 571776.0 563405.1 61568.0 58090.4 759 27.963 26 16.290 44.253 6464.0 10176.0 6452.6 0.0 880512.0 401761.5 571776.0 565630.1 61568.0 58090.4 760 28.016 26 16.290 44.306 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 6464.0 10176.0 6452.6 0.0 880512.0 876197.9 571776.0 565630.1 61568.0 58093.7 760 28.016 26 16.290 44.306 9024.0 4928.0 0.0 0.0 840704.0 321404.4 567360.0 469081.9 61440.0 58096.0 761 28.034 27 17.329 45.363
Voici quelques explication sur la signification des entêtes de colonne concernant les zones :
- La première lettre indique la zone : S0 et S1 = survivor 1 et 2, E = Eden, O = Old et P = Permanent
- La dernière lettre indique la mémoire réellement occupée ou la taille réservée : C = Capacity et U = Used
Concernant le GC : YGC = Young GC, YGCT = Young GC Time, FGC = Full GC, FGCT = Full GC Time et GCT = GC Time (temps global)
On voit tout d’abord, que S0 et S1 sont remplis l’un ou l’autre mais pas les deux en même temps. En regardant la colonne YGC, on voit qu’a chaque incrémentation, S0 et S1 permuttent. La taille de l’Eden Space varie également à chaque passage du YGC. On voit également qu’à chaque YGC, le OU augmente afin de stocker les objets les plus anciens de l’Eden Space. Enfin on voit qu’à l’avant dernière ligne, le OU se rapporchait du OC ce qui laissait présager de la saturation de la zone Old Space. La dernière ligne indique donc le passage du FGC qui a eu pour effet de nettoyer le Old Space et de lui réassigner une nouvelle taille.
Que se passe-t-il en cas de fuite mémoire (autrement dit d’objets restant alloués par erreur) ? Le OC grossit de plus en plus, une fois arrivé à son max, le OU se rapproche de la limite. Le FGC passe donc fréquemment au point de ralentir la JVM… puis une belle exception OutOfMemory est déclenchée.
Analyse d’une JVM
Jstat vous permet d’observer l’occupation mémoire, et voir que vous avez effectivement un problème… Mais comment savoir d’où ça vient ? Il existe plusieurs outils, je vous citerai les 2 que j’utilise : jmap et mat.
Jmap est un outil du même style que jstat. Il est livré avec le JDK et permet d’analyser une JVM en cours de fonctionnement. Il permet dans un premier temps d’obtenir une liste des objets actuellement en mémoire, triée par occupation mémoire décroissante. Bien entendu les premières places sont occupées par des objets comme String, byte[], etc, mais si vous voyez une de vos classes dans les 20 premières positions… c’est louche. Pour obtenir cette liste, voici la commande :
jmap -histo <pid>
Si cette liste ne suffit pas à trouver le fautif, vous pouvez utiliser l’artillerie lourde : générer un dump binaire et utiliser un outil qui analysera ce dump vous permettant de naviguer dans vos objets en mémoire. Un dump peut être généré de différentes façons (par exemple jmap ou jconsole). Voici la commande jmap qui génère ce dump :
jmap -heap:format=b
Cette commande correspond à un JDK5 et peut être différente avec un JDK d’une autre version. Attention, si la génération de la liste ne prend que quelques secondes, la génération d’un dump est beaucoup plus longue. Inutile de vous dire que pendant ce temps la JVM est bloquée… donc pour une JVM en exploitation, il vaut mieux le savoir
Différents outils d’analyse existent également. J’utilise pour ma part MAT qui est un plugin Eclipse et existe également dans version autonome. Cette dernière est préférable pour des question d’occupation mémoire (il faut beaucoup de mémoire pour analyser le dump). Cet outil permet d’analyser le dump et de vous suggérer des problèmes possibles. Si cela ne suffit pas (ou si vous pensez qu’il se trompe) vous pourrez alors analyser les objets, trouver leur référence, voir leur contenu, etc.
6 Responses to “Analyser la mémoire d’une application Java”
C’est clair qu’il vaut mieux utiliser MAT que jhat…
Sinon, voici une option bien pratique pour générer automatiquement un bump en cas de OutOfMemory : -XX:+HeapDumpOnOutOfMemoryError .
En cas de fuite de mémoire « lente » et de forte charge sur l’application, cette option n’est toutefois pas tellement utile car l’application exécute d’abord une succession de full GC ce qui rend le service inutilisable bien avant de rencontrer le OutOfMemory. Dans ce cas, la génération manuelle du dump est appréciable.
Super post!
[...] C’est technique, mais utile pour qui fait du Java : le billet de Bertrand Peralta sur l’analyse de la mémoire utilisée par une application Java. [...]
Intéressante synthèse
On retrouve le monitoring de ces différentes zones mémoires via l’API JMX (depuis Java SE 1.5).
Aussi, peut-être un point m’échappe-t-il, mais je n’ai pas connaissance d’un moyen d’analyser la mémoire de DEUX applications JAVA s’exécutant en parallèle sur la même JVM ; est-ce correct ?
Effectivement, ces outils analysent une JVM donc je ne pense pas qu’ils sachent faire la distinction entre deux application se partageant une seule JVM.
Leave a Reply