Memory Analyzer (MAT) : pistons les fuites mémoire

Alors que l’on nous a toujours expliqué que Java s’occupait tout seul du nettoyage des objets inutilisés avec son Garbage Collector, tous les développeurs ont un jour, sur l’un de leurs projets, découvert qu’ils pouvaient faire une fuite de mémoire avec comme sentence le fatal : OutOfMemoryException.

Cette erreur est d’autant plus difficile à analyser que les développements ne se font plus de zéro mais sont souvent l’accumulation de différents frameworks le tout étant déployé dans un serveur d’applications.

Heureusement pour nous, des outils existent pour y voir plus clair, Optimize It a été l’un des précurseurs, les plus connus actuellement sont YourKit, JProfiler ou JProb
Ce sont des produits commerciaux de qualité, mais les développeurs n’ont pas toujours la possibilité de les avoir.
Et dans le monde du gratuit ?
Sun propose quelques outils : jmap, jhat et VisualVM
Leur utilisation permet de commencer l’étude, mais une analyse fine ressemble vite à trouver l’aiguille dans une botte de foin, surtout quand la botte dépasse le giga octet.

Lors d’une mission récente où l’un de ces outils pouvait se révéler vite indispensable, une recherche sur le web m’a fait découvrir un nouveau venu dans le monde open source : Memory Analyzer (MAT).

Découverte de Memory Analyzer

Memory Analyzer est un produit en phase d’incubation parmi les projets Eclipse.org, le contributeur est SAP (une grosse référence dans le monde des ERP).
Son objectif est l’analyse les images mémoire (Dump au format HProf).
La première version est apparue début 2008, la version 0.7 actuelle est sortie en décembre 2008.

Malgré cette jeunesse, le produit est déjà riche en fonctionnalités, il apporte des fonctionnalités intéressantes :

  • Des analyses automatiques permettant de dégrossir le travail d’étude de la mémoire
  • La possibilité de voir les instances avec leurs propriétés, leur référence en offrant une navigation aisée à ce niveau.
  • Il permet de faire du requêtage objet au format OQL. Très pratique pour faire des recherches précises sur un nombre très important d’instances.
  • Le premier lancement de Memory Analyzer précalcule des graphes de consommation. Ce calcul permet par la suite d’avoir une information rapide sur la taille de l’objet courant et de tous les objets retenus (Retained Heap Size)
  • L’indexation initiale permet de travailler rapidement sur de grosses images mémoires, 512 Mo de RAM ont été suffisants pour travailler avec un dump de 1,5Go. L’utilisation d’une JVM 64 bits est possible pour de très gros fichiers.
  • Des fonctions plus traditionnelles : Parcours des objets, recherche du point d’attache (GC root), liste des objets retenus par classe ou par instance…

Récupération du dump

Plusieurs possibilités :

  • Ajouter le paramètre JVM: -XX:+**HeapDumpOnOutOfMemoryError **-XX:HeapDumpPath= Le dump ne sera créé qu’en cas de manque de mémoire, cette option est disponible depuis les JVM 1.4.2
  • Ajouter l’agent java hprof: –agentlib:hprof=heap=dump,format=b,file=
    Le dump peut être créé par un CTRL+BREAK sous Windows ou un kill -3 sous Unix, cette option est disponible depuis les JVM depuis les JVM 1.4.2.
  • Utiliser jps pour obtenir l’id du processus Java puis **jmap **-dump:format=b,file= Cet outil est disponible depuis la JVM 1.5 sous Sun et Linux et la JVM 1.6 pour les autres OS

Lancement

Memory Analyzer peut être utilisé seul sous la forme d’Eclipse RCP ou intégré dans votre Eclipse sous forme de plugin.
Les deux types d’installation sont disponibles à cette adresse : http://www.eclipse.org/mat/downloads.php

Une fois ce fichier obtenu, vous pouvez lancer Memory Analyzer
Suivant la taille du dump, il est recommandé de lui donner le maximum de mémoire possible en ajoutant -Xmx1024m:

  • En version RCP, dans le fichier MemoryAnalyzer.ini
  • En version plugin, dans le fichier Eclipse.ini

Dans la version plugin, il faut activer la perspective Memory Analysis

Comprendre les bases du Garbadge Collector

Pour comprendre les résultats de Memory Analyzer, il faut comprendre les bases de la mécanique du Garbage Collector.
Chaque instance de classe a une taille en mémoire, cette taille appelée "Shallow Heap" est constituée des types primitifs (int, char …).
Une instance peut avoir également des références sur d’autres objets qui ont eux même une taille.
Les objets peuvent être liés entre eux. Cela constitue un graphe.

Il y a des racines d’attache des graphes dans les JVM, c’est le cas des ClassLoader qui tiennent les références des attributs statiques, il y a aussi les Threads …
Ces racines s’appellent des "GC root" dans le produit.

Toutes les instances en mémoire finissent par être attachées à une racine, si ce n’est pas le cas, le Garbage Collector peut supprimer le graphe.

Le travaille de Memory Analyzer au premier lancement consiste à analyser chaque instance en mémoire.
Pour chacun, il analyse si sa disparition aurait entraîné la disparition d’autres objets, si c’est le cas, il lui ajoute en "Retained Heap" la taille de ces derniers. Cette analyse lui permet d’obtenir un arbre de dominant "Dominator Tree"

Pour illustrer tout ceci, un exemple simple avec un cache d’entreprise qui techniquement est un attribut statique de type Map.

public class CacheEntreprise { private static Map entreprises = new HashMap(); ... }

Cet objet contient une liste d’entreprises qui référence des salariés qui ont eux-mêmes des liens sur d’autres objets. Les objets Salarie ont une référence inverse sur Entreprise, ces deux objets sont donc liès dans les deux sens:

public class Entreprise { private List salaries = new ArrayList(); ... } public class Salarie { private Entreprise entreprise; ... }

imagebrowser image

Après son analyse, il obtient l’arbre dominant suivant, il a déterminé qu’Entreprise était dominant par rapport à Salarie :
imagebrowser image

Pour finir de complexifier tout cela, Java 5 a apporté une modification sur les types de référence sur les instances.
Il y avait les références fortes "StrongReference"
Il y a maintenant les références douces "SoftReference": L’objet est référencé, mais si la JVM a besoin de mémoire, elle peut libérer cet objet.
Enfin, on trouve également les références légères "WeakReference": L’objet est référencé, tant que quelqu’un d’autre à une référence forte sur lui, sinon, il est nettoyé.

Memory Analyzer est capable de travailler en prenant en compte ou pas ce type de références.

L’application simpliste

En prenant l’exemple simpliste de la modélisation précédente : CacheEntreprise, Entreprise, Salarie et Adresse.
L’histogramme des classes nous montre ceci:
imagebrowser image

On peut voir 2535 objets Entreprise de 60 ko, retenant 206 Mo de donnés (les salariés et les adresses)
Le CacheEntreprise n’a pas d’instance ici, car c’est uniquement une propriété statique qui a été utilisée.

On peut utiliser des expressions régulières pour n’afficher que les classes de notre projet :
imagebrowser image

On peut rechercher dans tout cela une entreprise en particulier avec l’OQL: exemple la société SAP AG

select * from test.mat.Entreprise where toString(raisonSociale) = "SAP AG"

imagebrowser image
Un seul objet d’affiche en bas à droite.
Les attributs de l’objet s’affichent en base à gauche

imagebrowser image
En dépliant l’objet et en suivant les références, en constate que l’entreprise pése 1008 octets, ils sont utilisés en majorité par les 3 salariés: 912 octets.
Les salariés ont une référence inverse sur l’entreprise, Memory Analyzer n’a pas comptabilisé cette taille sur le salarié avec son calcul dominant/dominé.
Le salarié sélectionné a une taille de 392 octets utilisés par ses attributs et son adresse, mais pas l’entreprise liée.

Un utilisant le parcours en référence inversée :

imagebrowser image

imagebrowser image

On peut voir que l’entreprise est référencée par les trois salariés avec l’attribut "entreprise" ainsi que le cache qui est lui-même tenu par le Class Loader de l’application.

L’autre moyen pour afficher le chemin le plus cours est de passer par "Merge Shortest Paths to GC roots"
imagebrowser image

imagebrowser image

Autre option intéressante, "Show Retained Set" affiche tous les objets retenus par la sélection :

imagebrowser image

imagebrowser image

Ces exemples donnent une idée de ce qu’il est possible de faire avec ce produit, avec un vrai projet vous vous en doutez c’est évidemment plus compliqué.
Voici quelques captures des rapports automatiques :
Les gros consommateurs de mémoires:
imagebrowser image
Ici: Les métas donnés de Spring, le cache d’Hibernate et les requêtes mémoires HSqlDB

Un autre rapport intéressant : les gros consommateurs par paquet Java

imagebrowser image

Conclusions

Ce jeune produit risque de vite devenir un incontournable pour les développeurs en manque de mémoire.

A l’utilisation, certaines ces fonctionnalités dépassent ces concurents, mais il se limite à l’analyse mémoire conjointement à HPROF.