Tutoriel pour comprendre comment se produit un OutOfMemoryError et comment le traiter

Antonio Gomes Rodrigues

Dans ce tutoriel nous étudierons les mécanismes d'apparition d'un OutOfMemoryError dans la JVM et comment les résoudre.

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez . ♪

- Titre : Tutoriel pour comprendre comment se produit un OutOfMemoryError et comment le traiter

- Auteur : Antonio Gomes Rodrigues

- Parution : 6 janvier 2016

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Antonio Gomes Rodrigues. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Définition d'un OutOfMemoryError

- II-A. Une utilisation excessive de mémoire

- II-B. Une fuite mémoire

- III. Architecture mémoire d'une JVM

- IV. Fonctionnement du Garbage Collector

- V. Comment se manifeste une fuite mémoire

- VI. Conclusion et remerciement

I - Introduction

Un des atouts (ou inconvénients 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émoire (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, au problème de performance, s'ajoute le problème de disponibilité de l'application pour les clients.

Plus globalement, sans vouloir être puristes, 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.

II - Définition d'un OutOfMemoryError

L'exception OutOfMemoryError est levée lorsque la JVM ne peut plus allouer de mémoire à 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.

II-A - 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.

II-B - 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 collections ;

- mauvaise gestion des caches ;

- mauvaise gestion des sessions HTTP ;

- problème avec les ThreadLocal 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.

III - 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.

IV - 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.

Ce sont des objets qui ont très peu de chances 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és.

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 au long de la vie de l'application pour que l'exception OutOfMemoryError apparaisse au bout d'un moment.

V - Comment se manifeste une fuite mémoire

Maintenant que nous savons comment apparaît 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 (les slides sont disponibles ici), plus il y a d'objets en mémoire, plus le GC met du temps à faire son travail.

Une autre chose à regarder est l'âge des objets (nombre de fois où ils ont survécu à 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).

VI - Conclusion et remerciement

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.

Nous tenons à remercier Phanloga pour sa relecture attentive de cet article.