Java et la gestion de la mémoire

Les problèmes de performance  les plus couramment rencontrés en Java peuvent provenir de quatre causes :

  • Le code de l'application
  • L'environnement sur lequel s'exécute l'application (serveur d'application, jvm, os)
  • Dépendances externes (base de données, web services, ...)
  • Charge excessive

Les causes ci-dessus sont données dans l'ordre de leur probabilité d'occurrence.

Il faut bien comprendre que l'upgrade du serveur physique n'intervient qu'en dernier lieu ! En effet, La cause est souvent logicielle avant d'être matérielle.

Dans ce billet, je vais rentrer plus en détail sur l'optimisation de la JVM Sun et plus particulièrement sur la gestion de la mémoire.

Comment fonctionne la mémoire en Java ?

Avant tout, il faut bien comprendre le fonctionnement d'une machine virtuelle Java. Au démarrage de la machine virtuelle, une certaine quantité de mémoire est allouée pour l'exécution. Cette mémoire est divisée en 2 grandes zones :

  • La zone "heap" : c’est la zone mémoire de travail.
  • La zone "permanent" : c’est la zone mémoire dans laquelle vont être stockées les classes java (les classes elles-même et non les instances) ainsi que les objets de type String qui ont été internalisés.

La "heap" est elle-même divisée en deux zones :

  • la zone "young"
  • la zone "tenured"

Enfin, la zone "young" est subdivisée en trois zones :

  • la zone "eden"
  • la zone "survivor 1"
  • la zone "survivor 2"

Le schéma suivant récapitule l’organisation de la mémoire dans la JVM.

Dans le schéma précédent, j’ai volontairement omis les zones « virtual » qui sont des zones mémoire inutilisées servant à réserver de la mémoire pour pouvoir faire grossir les différentes zones dynamiquement.

Le cycle de vie des objets en mémoire est le suivant :

  1. Les objets sont créés dans la zone "eden"
  2. Lorsque la zone "eden" est pleine, les objets vivants sont déplacés dans la première zone "survivor" et les objets morts sont supprimés.
  3. Lorsque la première zone "survivor" est pleine, les objets vivants sont déplacés dans la seconde zone "survivor" et les objets morts sont supprimés.
  4. Lorsque la seconde zone "survivor" est pleine, les objets vivants sont déplacés dans la zone "tenured" et les objets morts sont supprimés.
  5. Lorsque la zone "young" est pleine et qu'il n'y a plus de place dans la zone "tenured", alors le ramasse miette parcourt la totalité des objets de la zone "tenured" et supprime les objets morts. Cette opération est couteuse et fige les processus en cours d'exécution dans la JVM.

Les objets présents dans la zone "tenured" doivent donc être des objets ayant un cycle de vie long. Minimiser l'exécution du ramasse miette dans la zone "tenured" est le premier levier de tunning mémoire d'une application Java.

En suivant ce processus, on pourrait penser que les objets ayant une durée de vie courte ont largement eu le temps d'être nettoyé lors du passage de la zone "eden à la première zone "survivor" puis à la seconde zone "survivor". Dans la pratique il s'avère que ce n'est pas souvent le cas, cela étant dû à un sous-dimensionnement de la zone "young" par défaut.

Mais quel est le paramétrage qui va faire marcher mon application ?

Il n’existe malheureusement pas de paramétrage tout fait et convenant à toutes les situations. Chaque application étant différente, le paramétrage devrait être effectué spécifiquement. De plus, les algorithmes de collection n’étant pas spécifiés, chaque JVM, voire chaque version de JVM peut réagir différemment.

Le principe de l’optimisation reste cependant le même.

  • Observer le comportement de la mémoire avec les paramétrages par défaut
  • Déduire de ces observations de nouveaux paramètres
  • Appliquer ces paramètres et les tester

C’est donc cette démarche itérative qui permet de trouver les paramètres mémoire spécifiques au bon fonctionnement d’une application.

Il existe différents outils permettant de surveiller la mémoire de la JVM. Parmi ceux-ci, on peut citer :

  • JConsole : c’est une console de surveillance de la JVM permettant de se connecter via JMX à une machine virtuelle java locale ou distante. Cette console est livrée avec les JDKs Sun depuis la version 5.
  • Java VisualVM : c’est une version améliorée de JConsole livrée avec les JDKs Sun depuis la version 6.
  • Les différents profilers du marché, commerciaux ou non
  • La sortie de la console

La sortie de la console s’active à l’aide de paramètres à passer au démarrage de le JVM.

Ci-dessous, voici des exemples de sorties de la console avec différents paramètres :

  • verbose:gc
    [GC 16837K->9473K(130176K), 0.0074487 secs]
    [GC 17662K->10411K(130176K), 0.0084880 secs]
    [Full GC  17409K->10722K(130176K), 0.1309334 secs]
  • XX:+PrintGCDetails
    [GC [DefNew: 8800K->889K(9152K), 0.0073819 secs] 16837K->9473K(130176K), 0.0074487 secs]
    [GC [DefNew: 9078K->960K(9152K), 0.0084220 secs] 17662K->10411K(130176K), 0.0084880 secs]
    [Full GC [Tenured: 9451K->10722K(121024K), 0.1308577 secs] 17409K->10722K(130176K), [Perm : 16383K->16383K(16384K)], 0.1309334 secs]
  • XX:+PrintGCTimeStamps
    5.806: [GC 5.806: [DefNew: 8800K->889K(9152K), 0.0073819 secs] 16837K->9473K(130176K), 0.0074487 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    5.972: [GC 5.972: [DefNew: 9078K->960K(9152K), 0.0084220 secs] 17662K->10411K(130176K), 0.0084880 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
    6.192: [Full GC 6.192: [Tenured: 9451K->10722K(121024K), 0.1308577 secs] 17409K->10722K(130176K), [Perm : 16383K->16383K(16384K)], 0.1309334 secs] [Times: user=0.14 sys=0.00, real=0.14 secs]

Que ne faut-il pas faire ?

Les deux principales mauvaises pratiques sont les suivantes :

  1. Utiliser la méthode System.gc() : Il est en effet déconseillé de forcer le lancement du garbage collector. Ce dernier est automatiquement optimisé à chaud en fonction du déroulement de l'application. Forcer l'exécution du garbage perturbe son fonctionnement.
  2. Doubler la quantité de mémoire allouée à la JVM : En effet, bien que ceci va sans doute régler le problème (et encore, pas dans tous les cas), le temps utilisé par le processus de GC est directement lié à la taille de la mémoire. Les performances risquent donc d'être inférieures.

Pour aller plus loin, vous pouvez consulter le site d’Oracle : http://www.oracle.com/technetwork/java/gc-tuning-5-138395.html.

Un commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.