rss
twitter
    Suivez-moi sur Twitter ! :)

Tomcat et la fameuse "OutOfMemoryError : PermGen space"


Qu’appelle-t-on "OutOfMemoryError : PermGen space" ?



Cette erreur se rencontre lorsque l'espace de la mémoire permanente (PermGen) de la machine virtuelle Java (JVM) a atteint son niveau maximum (ie. sur une JVM Sun).

Les définitions de classe sont stockées au sein de la mémoire permanente. Cette erreur est levée lorsque l'espace restant n'est plus suffisant pour accueillir les nouvelles définitions de classe à charger.

NB: les instances sont stockées dans une zone mémoire différente ("Heap" space) qui correspond à une autre erreur.


Quand rencontre-t-on une "OutOfMemoryError : PermGen space" ?



On rencontre par exemple cette erreur après de multiples redéploiements d'une WebApp Tomcat comportant des "fuites" mémoire... On va redéployer la WebApp 10 fois sans problème, puis on perd Tomcat à la 11ième à cause de cette erreur. Bien souvent cette erreur pose de sérieux problèmes en production où la tendance est plutôt à redémarrer Tomcat tous les soirs pour éviter tout problème (plutôt que de corriger les soucis de fuites).


Un excellent papier rédigé par Franck Kieviet (en anglais, mais très visuel ;) ) explique comment se provoque une fuite mémoire (ie. déchargement impossibles d'objets/de définitions de classes) et cite les exemples les plus courants de fuites mémoire :
   Dangling thread, Commons logging, java.util.logging.Level, Thread context classloader, new Thread(), Bean util, Details, SQL Driver


Quels sont les symptômes d'une "OutOfMemoryError : PermGen space" ?



Peut-être ne l'avez-vous jamais rencontré, alors comment savoir si votre WebApp est sujette aux fuites de mémoire (donc à terme à cette erreur) ?


Des outils permettent de visualiser l'occupation des différents espaces mémoire :

  • JConsole : application visuelle qui se rattache à une JVM (un process Java) et qui (entre autres nombreuses fonctionnalités) affiche les courbes : espace occupé par les données (heap), nombre de définitions de classes actuellement en mémoire, total du nombre de classes chargées et déchargées depuis le lancement de JConsole, nombre de thread, etc..

  • jmap : un outil qui se rattache également à une JVM et qui est capable (entre autres) de faire des extractions de la mémoire pour en ressortir des statistiques (ie. liste des définitions de classes en mémoire). Jmap permet également de faire un dump de cette mémoire pour jhat.

  • jhat : un outil capable de lire un dump créé par jmap et de créer un mini serveur Web pour naviguer dans cette mémoire (utilisateurs avertis!!).




Pour visualiser le problème, je conseille JConsolearticle de sun (en).

Mais comment utiliser JConsole ?



Il faut autoriser JConsole à pouvoir se rattacher à la JVM à osculter.

Comment ? voici un exemple :
 ajoutez les options suivantes à la JVM de votre application (fonctionne aussi pour Tomcat) :

-Dcom.sun.management.jmxremote.port=9090
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false


Déterminez ensuite quel est le numéro du processus Java correspondant à votre application (Tomcat correspond à la ligne jps "Bootstrap") :

$ ps -xaf | grep java
ou
$ jps



Une fois le numéro de processus récupéré, lancer JConsole (sous un environnement graphique, avec un utilisateur autorisé, (ie. tomcat)...) :

# jconsole &

Choisissez le menu "New" / "Connexion" puis le processus correspondant à votre application.


Voici un aperçu de JConsole pour plusieurs déploiements d'une WebApp comportant une fuite mémoire (ou mal paramétrée) :



   on peut constater (encadré en bas à gauche : mémoire permanente), après de nombreux redéploiements, qu'aucune classe n'a été déchargée. Ce genre de comportement se termine par une erreur.


Voici un aperçu de JConsole pour plusieurs déploiements d'une WebApp ne comportant plus de fuite mémoire (amha) :

   on peut constater (encadré en bas à gauche : mémoire permanente), après de nombreux redéploiements, des classes ont été déchargées.

   A partir du moment où, malgrès le nombre de redéploiements, la courbe forme une sorte de plateau (nombre de classes maximum impossible à dépasser, ici 16000 classes), on peut supposer que la WebApp est "stable" au niveau occupation de la mémoire permanente.

   PS: il semblerai que sur la SDK de Sun, le déchargement de classe ne soit pas systématique, mais pratiqué que lorsque nécessaire. Cela expliquerai pourquoi le déchargement de classe n'intervient pas lors des tous premiers redéploiements (ie, dans mon cas j'ai constaté une dizaine de redéploiements).



Comment se protéger de l'"OutOfMemoryError : PermGen space" ?



C'est bien ici la question la plus intéressante de ce post...

Voici une liste (non-exhaustive) des moyens de se protéger de cette erreur :

  • Tenter d'identifier les librairies / classes utilisées qui ont des fuites de mémoire : solution la plus satisfaisante, évidemment, mais qui peut prendre beaucoup de temps...

  • Utiliser un autre SDK (ie.IBM) : sans le désir de polémiquer, j'ai déjà remarqué et lu à plusieurs reprises que ce problème était moins fréquent sur une autre JVM ... : solution assez peu satisfaisante

  • Augmenter la mémoire allouée : votre serveur ne mettra plus 3j à tomber mais 5 ou 10 ? : voici solution pour les rois de la bricole (marche aussi pour les hommes pressés) ...

  • Isoler la WebApp dans une JVM dédié: (post sur le sujet) ce point n'est pas réellement une mesure de protection sur l'application elle-même, mais peut être une très bonne mesure à prendre pour éviter de contaminer plusieurs applications à cause d'une seule qui pose problème...



Identifier les fuites mémoires



Après plusieurs essais avec jmap et jhat, et navigation dans le dump de la mémoire, j'ai perdu un peu mes espoirs de trouver une solution toute faite pour identifier les fuites mémoire.

En réalité, jmap sera pour moi amplement suffisant..


En préambule : lancez votre WebApp et effectuez plusieurs redéploiements (s'arrêter avant l'exception PermGen).

Sous réserve d'un paramétrage et d'un utilisateur correct (cf exemple de JConsole ci-dessus) la commande suivante affiche les objets chargés en mémoire et enregistre le résultat dans le fichier JVM_MEMORY.

jmap -histo PID_OF_TOMCAT > JVM_MEMORY


Suivant le nombre d'objet, la liste retournée peut être très longue.

Pour chaque définition de classe présente en mémoire, 4 colonnes sont affichées sur une ligne : un id, un nombre d'instances, une taille occupée (bytes), le nom de la classe.

Il s'agit de repérer les fuites en isolant les répétitions trop nombreuses de définition de classes (plusieurs lignes avec un même nom de classe).


Voici des exemples de définitions multiples rencontrées :

com.mysql.jdbc.VersionedStringProperty
com.mysql.jdbc.SingleByteCharsetConverter
com.mysql.jdbc.Driver
com.mysql.jdbc.log.NullLogger
apache.log4j.helpers.NullEnumeration
org.apache.log4j.DefaultCategoryFactory
org.apache.log4j.or.RendererMap
org.apache.log4j.spi.RootLogger
org.apache.log4j.Hierarchy
org.apache.log4j.Level
org.apache.log4j.ProvisionNode
org.apache.log4j.CategoryKey
org.apache.log4j.Logger
org.apache.commons.dbutils.BasicRowProcessor



Pour constater un problème de fuite mémoire avec MySQL par exemple, voici une méthode que j'ai utilisé :

jmap -histo PID_OF_TOMCAT | grep "com.mysql.jdbc.Driver" | wc -l

Cette commande affiche le nombre de définition en mémoire pour la classe Driver.

J'effectue un redéploiement de la WebApp et je rejoue la même commande.

jmap -histo PID_OF_TOMCAT | grep "com.mysql.jdbc.Driver" | wc -l


Si le nombre retourné augmente d'un, alors c'est qu'aucune classe n'a été déchargée.


Libérer la mémoire !


Voici les différentes solutions que j'ai utilisé pour libérer de la mémoire permanente.

Tout d'abord, et c'est essentiel, il faut autoriser la JVM à décharger des classes. Ajouter l'option suivante à votre application :

-XX:+CMSClassUnloadingEnabled


Ensuite, vous pouvez paramétrer la taille max de votre mémoire permanente (par défaut à 64M je crois, et n'oubliez pas l'unité "M"):

-XX:MaxPermSize=80M


Ensuite, libérez la mémoire en détruisant notamment :

  • les drivers JDBC montés en mémoire

  • les threads non terminés

  • les singletons

  • ...



Vous pourrez pour cela ajouter un listener à votre WebApp (cf. java-tips (en)) et libérer la mémoire lorsque le contexte est détruit ("contextDestroyed(ServletContextEvent event)").

Sur le wiki de Tomcat, le problème est abordé et certaines méthodes sont également évoquées.


Voici les méthodes que j'ai pu retenir.

Exemple pour libérer les singletons :

if (MySingleton.isInstance()) {
MySingleton.getInstance().destroy();
}


Exemple pour libérer "common-logging" :

// cf. http://wiki.apache.org/jakarta-commons/Logging/FrequentlyAskedQuestions
System.out.println("Shutdown LogFactory (common-logging) !");
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
LogFactory.release(contextClassLoader);


Exemple pour libérer "log4j" :

// cf. http://wiki.apache.org/logging-log4j/UsefulCode
System.out.println("Shutdown LogManager (log4j) !");
LogManager.shutdown();



Exemple pour libérer les drivers JDBC :

Enumeration loadedDrivers = java.sql.DriverManager.getDrivers();
while (loadedDrivers!= null && loadedDrivers.hasMoreElements()) {
Driver d = loadedDrivers.nextElement();
log.info("Unregister Driver " + d.toString());
try {
DriverManager.deregisterDriver(d);
} catch (SQLException sqle) {
log.error("Exception while unregister Driver: " + sqle.getMessage());
}
}




Merci de me faire vos remarques si ce sujet comporte des erreurs.. si vous voyez d'autres astuces je les ajoute.