OutOfMemoryError Java, que faire ?

Introduction

Un des atouts (ou inconvénient pour certains) de Java est qu'il n'y a pas à s'occuper de la libération de la mémoire grâce au Ramasse Miettes (Garbage Collector).

Malgré cela, nous ne sommes pas à l’abri de fuites mémoires (la fameuse exception OutOfMemoryError).

Pour pallier à ce problème, le moyen le plus simple est de faire un arrêt/relance de l'application. Solution que je déconseille d'utiliser, car en plus du problème de performance, s'ajoute le problème de disponibilité de l'application pour les clients.

Plus globalement, sans vouloir être puriste, nous n'avons pas d'autres choix que de corriger l’OutOfMemoryError, que cela soit un mauvais paramétrage de la JVM et/ou une fuite mémoire.

L'objectif de cet article est de comprendre comment apparaissent les OutOfMemoryError.

Pour un cas pratique,  Stéphane Chabrier vous a présenté "comment découvrir et isoler une fuite mémoire java" dans ses 2 articles (1er partie , 2ème partie).

Définition d'un OutOfMemoryError

L'exception OutOfMemoryError est levée lorsque la JVM ne peut plus allouer de mémoire pour un objet.

Il existe plusieurs types d’OutOfMemoryError (liste non exhaustive) :

  • Exception in thread "main": java.lang.OutOfMemoryError: Java heap space
  • Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  • Exception in thread "main": java.lang.OutOfMemoryError: PermGen space
  • Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  • Exception in thread "main": java.lang.OutOfMemoryError: requestbytes for. Out of swap space?
  • Exception in thread "main": java.lang.OutOfMemoryError:(Native method)

De cette définition, nous pouvons en déduire que les causes d'un OutOfMemoryError peuvent être de deux types.

Une utilisation excessive de mémoire

La cause la plus simple est que l'application testée utilise trop de mémoire par rapport à la configuration de la JVM.
Pour résoudre ce problème il faut :

    • modifier les paramètres mémoire (Xmx, Xms...) de la JVM afin d'allouer plus de mémoire

  vous trouverez plus d'informations pour la JVM d'IBM ici

    • analyser l'utilisation de la mémoire avec par exemple un profiler afin de réduire la consommation mémoire

 vous trouverez plus d'informations pour le profiler YourKit Java Profiler ici.

Une fuite mémoire

L'application a une fuite mémoire, c'est-à-dire que des objets non utilisés par l'application et non collectés par le Garbage Collector de la JVM sont présents en mémoire. Ces objets n'étant jamais collectés, ils vont remplir plus ou moins rapidement la mémoire jusqu'à ce que l'exception OutOfMemoryError apparaisse.

Les causes de cette fuite de mémoire peuvent être nombreuses :

  • mauvaise utilisation des Collection ;
  • mauvaise gestion des caches ;
  • mauvaise gestion des sessions HTTP ;
  • problème avec les Thread Local variables ;
  • tentative d'allocation d'objets beaucoup trop volumineux ;
  • etc.

Afin de comprendre au mieux ce mécanisme, regardons d'un peu plus près comment marche la JVM.

Architecture mémoire d'une JVM

Pour ceux qui utilisent la JVM d'IBM, je leur conseille de lire cet article.

Pour la HotSpot d'Oracle, de manière simplifiée, son architecture mémoire est la suivante.

Une fuite mémoire peut être dans n'importe quelle partie, y compris la PermGen.

Fonctionnement du Garbage Collector

Pour le fonctionnement global du Garbage Collector sur la JVM HotSpot, je vous conseille de lire la partie 3.3 - Consommation mémoire de cet article.
L'étape qui nous intéresse est l'étape Mark Phase qui différencie les objets vivants des objets morts.

L'algorithme de l'étape Mark Phase est le suivant.
La JVM sélectionne des objets spéciaux appelés GC Root.
Ces objets sont des objets qui ont très peu de chance d'être collectés à ce moment (par exemple : référence JNI, thread en cours d'exécution, class chargées par le ClassLoader, variables de méthodes en cours d'exécution...)
Tous les objets qui ne référencent pas directement ou indirectement un GC Root sont marqués inutilisé.



Lors de l'étape Sweep, le Garbage Collector libère la place mémoire utilisée par les objets marqués inutilisés.

Donc tant qu'un objet a une référence sur un GC Root, il n'est pas collecté. Or si cet objet n'est plus utilisé par l'application, on se retrouve avec un objet inutile en mémoire. C'est ce que l'on appelle une fuite mémoire en Java.
Il suffit que ce type d'objet soit créé tout le long de la vie de l'application pour que l'exception OutOfMemoryError apparaisse au bout d'un moment.

Comment se manifeste une fuite mémoire

Maintenant que nous savons comment apparait une fuite mémoire, regardons comment elle se manifeste.

Comme nous l'avons vu précédemment, un OutOfMemoryError n'est pas forcement levé à cause d'une fuite mémoire. Donc dans un premier temps il est important de bien configurer la mémoire de la JVM afin d'éliminer l'hypothèse d'une utilisation excessive de mémoire. Après ça, il y a de fortes chances que la présence d’OutOfMemoryError provienne d'une fuite mémoire.

Malheureusement, il y a d'autres symptômes qui sont beaucoup moins évidents à détecter, en particulier pour une petite fuite mémoire qui met plusieurs semaines à remplir la mémoire.

Ce qui peut nous mettre la puce à l'oreille pour ce genre de fuite de mémoire est la forme de la courbe de la taille de la mémoire utilisée lorsque l'application tourne.

Si vous rencontrez ce genre de courbe, il est possible que cela soit dû à une fuite mémoire.

Sinon un autre symptôme est le ralentissement de l'application au fur et à mesure du temps.
Car comme je l'ai dit lors de la breizhcamp 2012 (slides sont disponibles ici), plus il y a d'objets en mémoire, plus le GC met du temps à faire son travaille.

Une autre chose à regarder est l'âge des objets (nombre de fois qu'ils ont survécus à un GC).

Par exemple dans le profiler de VisualVM, c'est la colonne Generations.

Plus l'âge est grand et plus cela fait longtemps que l'objet est en mémoire et plus les chances que cela soit une fuite mémoire sont grandes (attention, tous ne sont pas des fuites mémoire, car il est normal que certains objets restent en mémoire longtemps).

Conclusion

Comme nous l'avons vu, malgré la présence d'un Garbage Collector, il existe quand même des fuites mémoires en Java.
De plus, l'exception OutOfMemoryError ne signifie pas forcement une fuite mémoire et donc la compréhension de l'application testée et du fonctionnement de la jvm sont indispensables pour résoudre ce type de problème.

Mon conseil est de faire des tests de vieillissement et des tests techniques (arrêt d'une partie des services...) lors d'une campagne de test de charge afin de minimiser les risques en production.

Laisser un commentaire

Merci d'effectuer cette opération simple pour valider le commentaire *

Mots-clés
RSS Feed