L’OutOfMemory est une des erreurs les plus célèbres de la JVM. Comme son nom l’indique, elle se produit quand la JVM n’a plus de mémoire. J’ai dernièrement rencontré cette erreur et cela m’a permis de découvrir les méandres peu connues de la mémoire de la JVM.
Tout d’abord petit rappel sur la répartition de la mémoire de la JVM:
- La Heap: Cette zone mémoire permet d’allouer les objets courants, c’est la zone mémoire la plus utilisée. Les options Xmx et Xms permettent de faire varier sa taille. Généralement les OutOfMemory viennent d’un problème dans cette zone.
- La PermGen: Cette zone est réservée au chargement des classes. L’option XX:MaxPermSize permet de définir sa taille maximum. Elle est régulièrement ajustée sur les conteneurs de Servlet contenant beaucoup de JSP.
Les outils fournis en standard sur la JVM tels que jstat, jmap ou plus évolués comme la jconsole et VisualVM permettent de collecter toutes les informations nécessaires sur ces zones de mémoire. Ils permettent aussi de faire des Heap Dumps pouvant être analysé avec jhat, VisualVM/netBean ou excellent HeapAnalyser.
Pour plus de précision sur ces outils vous pouvez relire le post de François et regarder ce tutorial
Une autre zone de mémoire existe dans la JVM, elle n’est pas bien décrite dans les différentes documentations, elle permet de stocker les piles mémoires des threads créées par la JVM. En effet, pour chaque thread créé à l’intérieur de la JVM, le système alloue un bloc mémoire pour stocker la pile d’exécution qui contient la liste des méthodes appelées ainsi que leurs contextes d’invocations et les informations propres au thread.
La taille de cette pile est par défaut de 512k et peut être modifiée avec l’option Xss ou ThreadStackSize. En cas de dépassement de cette taille, une StackOverflowError est lancée. Il est possible de voir une partie de ces informations dans les “StackTrace” des exceptions.
Les piles des différents threads sont stockées dans le même processus que la JVM mais ne sont pas prises en compte dans la taille de la Heap et de la PermGen; elles viennent donc s’additionner à la taille du processus système de la JVM.
Or cette taille mémoire n’est pas extensible à l’infini. En fonction de l’OS et de l’architecture de la machine, il existe une taille maximum qui ne peut pas être dépassée (sur une Redhat 32bit cette taille est d’environ 3Go ). Une fois cette limite atteinte, toute création de thread entraine un OutOfMemoryError: unable to create new native thread
Xmx + MaxPermSize + (Xss * nombre de thread) = Max Mémoire pour un processus sur l’OS
Plusieurs causes peuvent entrainer une surconsommation de thread:
- Le thread Leak: ce problème est peu courant mais peut arriver. C’est le même principe que pour le memory leak mais pour les threads. Une portion de code crée un thread qui ne s’arrête jamais. A chaque exécution du code, un nouveau thread est créé jusqu’au OutOfMemory: unable to create new native thread
- Une mauvaise configuration des connecteurs et des pools de threads: La somme des maximums des capacités des connecteurs et des pools de threads ne doit jamais dépasser le nombre de thread maximum de la JVM. Il est même plutôt conseillé de garder une bonne marge de sécurité pour prévenir des créations de thread faites par certains frameworks. Pour donner un ordre d’idée, Mark Thomas lors des rencontres Spring, a conseillé d’avoir en prod des connecteurs Tomcat allant de 200 à 800 threads.
Pour tous ces problèmes, il est possible de faire des threads dumps de l’application à intervalle régulier, de les comparer afin de trouver les nouveaux threads et d’identifier leur provenance (généralement le nom et le code exécuté sont de bons indices).
Pour faire des dumps , on peut utiliser jstack et visualvm et pour les étudier visualvm ou thread tda ( voir le post d’Emmanuel ). La jconsole et visualvm permettent aussi de surveiller le nombre total de thread de la JVM en temps réel, ce qui peut être d’une grande aide.
Si après tous ces tests, vous vous rendez compte que l’application a réellement besoin d’un nombre très important de thread pour subvenir à la demande, plusieurs solutions sont à envisager:
- 1 ère solution: la moins coûteuse, modifiez (si cela est possible) le Xmx et/ou le Xss de votre application de manière à pouvoir créer encore plus de thread. Il est ensuite conseillé de bien re-tester votre application.
- 2 ème solution: modifiez votre architecture physique et/ou logique: - si vous êtes sur des machines 32bit, passez en 64 bit de manière à repousser la limite de taille des processus.
- Modifiez votre application de manière à pouvoir ajouter d’autres instances de la JVM. Rendez l’application plus scaleable horizontalement.
- Transformez, si vous le pouvez, vos traitements synchrones, en traitements asynchrones de manière à mieux contrôler vos ressources et limiter les temps d’attente de vos threads.
Pour conclure, le nombre de thread dans une JVM n’est pas extensible à l’infini. Il est utile de garder cette règle à l’esprit quand on déclare des connecteurs HTTP, des pools de thread ou lors de la création d’un thread, cela pourra peut-être vous éviter de voir un beau matin dans vos traces des OutOfMemory: unable to create new native thread 😉
Plus d’informations sur le wiki de JBoss