I. Introduction

Il existe différentes JVM (JRockit, Hotspot, J9, Zing...) dans le monde de Java. Chacune a ses particularités et lors d'un audit de performance il est important de les connaître. Nous allons étudier le fonctionnement de la JVM J9 d'IBM livrée avec WebSphere 8.0 32 bits sur une plateforme x86. Dans mon cas, c'est la version JRE 1.6.0 IBM Windows 32 build pwi3260_26fp1-20110419_01 (FP1) (java –fullversion).

Attention, la JVM J9 n'est livrée avec WebSphere 8 que sur les environnements où elle existe. Dans les autres cas, c'est la JVM d'Oracle (Hotspot) qui est livrée.

II. Fonctionnement de la JVM

Il y a deux choses importantes :

  • l'architecture de la mémoire de la JVM ;
  • la gestion des objets par le GC.

On peut y ajouter la gestion des allocations des objets par l'élément Allocator. Regardons cela d'un peu plus près.

II-A. Architecture de la mémoire

La mémoire est découpée en deux parties (Heap et Stack) qui ont chacune leur utilité.

Image non disponible

II-A-1. Heap/Tas

La Heap (ou tas) est l'espace mémoire où tous les objets sont stockés. Lorsque la JVM est initialisée, la taille de la Heap est initialisée à la valeur spécifiée par -Xms et elle variera dynamiquement entre la taille minimale (-Xms) et la taille maximale fixée par le paramètre -Xmx.

Si le programme demande plus de mémoire que le maximum (-Xmx), la JVM générera un message d'erreur : java.lang.OutOfMemoryError : Java Heap space

Image non disponible

Il existe trois types de configuration de la Heap pour la JVM d'IBM en version 32 bits.

II-A-1-a. Continous Heap

Comme son nom l'indique, ici la Heap est composée d'un seul bloc continu de mémoire.

Image non disponible

Ce type d'architecture est conseillé par IBM pour certains types de batch où le débit doit être important.

II-A-1-b. Generational Heap

Les ingénieurs des JVM ayant remarqué que la durée de vie des objets en mémoire n'étant pas la même (par exemple, les objets créés dans une boucle n'existent pas longtemps) et qu'une grosse Heap pose des problèmes de performance, ils ont décidé de partitionner la Heap en deux parties en fonction de la durée de vie des objets.

Image non disponible

C'est la configuration par défaut depuis WebSphere 8.

Sur les anciennes versions, on peut l'activer avec l'argument -Xgcpolicy:gencon

II-A-1-b-i. Nursery Space/New Area

La Nursery Space (aussi appelée New Area) est la zone mémoire où sont alloués dans un comportement normal les nouveaux objets. Les objets y resteront tant qu'ils n'auront pas atteint l'âge Tenure age (nombre de fois où l'objet survit à un GC) ou qu'il reste de la place.

Sa taille est configurée par le paramètre -Xmn.

Elle est partitionnée en deux zones appelées Allocate Space et Survivor Space dont le ratio peut varier au cours du temps.

Image non disponible

Ces deux zones mémoires sont utilisées lors du Scavenge GC.

II-A-1-b-ii. Tenured space/Old Area

La Tenured Space (aussi appelée Old Area) est la zone mémoire où les « vieux objets » se retrouvent après un certain nombre de Scavenge GC ou lorsqu'il n'y a plus de place dans la Nursery Space.

Image non disponible
II-A-1-b-ii--. LOA/SOA

La Tenured space peut dans certains cas être décomposée en deux parties appelées LOA (Large Object Area) et SOA (Small Object Area). La zone de mémoire LOA permet de stocker les gros objets.

Image non disponible

La taille de la LOA est définie par les paramètres -Xloainitial, -Xloaminimum et -Xloamaximum et pourra être modifiée en fonction des besoins par la JVM après chaque GC. On peut la désactiver à l'aide du paramètre -Xnoloa.

II-A-1-c. Heap : Split Heap

Un des défauts de la Generational Heap est qu'elle doit être allouée de manière continue en mémoire. Or, cela peut poser des problèmes si l'on veut une grosse Heap sur un Windows 32 bits (limitée à moins de 2Gb).

Pour résoudre ce problème, les ingénieurs d'IBM ont créé la Split Heap qui est architecturée exactement comme une Generational Heap mais la Nursery Space et la Tenured Space sont séparées physiquement en mémoire.

La Tenured Space se retrouve dans les régions basses de la mémoire et sa taille maximale sera paramétrée avec -Xmox. Au contraire, la Nursery Space se retrouve dans les régions hautes de la mémoire et sa taille maximale sera paramétrée avec -Xmnx.

Image non disponible

Elle s'active avec le paramètre -Xgc:splitheap et n'est disponible que sur les versions 32 bits de Windows.

Exemple de paramétrage : -Xgc:splitheap -Xmx1500m -Xmox1300m

Lors de la création de la Split Heap, on pourra se retrouver avec ces messages d'erreurs :

  • JVMJ9GC056 Failed to allocate old space ;
  • JVMJ9GC056 Failed to allocate new space ;
  • JVMJ9GC056 Required split heap memory geometry could not be allocated.

II-A-2. Stack/Pile

La pile (stack) est la zone de mémoire utilisée pour la gestion de la pile d'appel.

Image non disponible

Cette zone se paramètre avec -Xss.

II-B. Allocation de mémoire

Regardons maintenant comment marche l'allocation de mémoire par le composant de la JVM appelé Allocator.

Le choix du type d'allocation va dépendre de la taille de l'objet et de la mémoire disponible dans la zone cible.

II-B-1. Cache allocation

Chaque thread a une zone mémoire dans la Heap qui lui est propre. Cette zone mémoire est appelée Thread Local Heap (TLH) et est représentée par un gros objet dans la Heap marquée non collectable (non sujette au Garbage Collector) associée à un thread. La taille de la TLH varie de 512 bytes à 128 KB en fonction de l'activité du thread associé (les threads allouant le plus d'objets auront les zones les plus grosses).

Image non disponible

Les objets de petite taille (moins de 512 bytes) seront alloués dans cette zone mémoire à l'aide du type d'allocation appelé cache allocation.

Une fois cette zone pleine, les objets contenus sont mis dans la Heap et un nouveau gros objet marqué non collectable associé à un thread est créé.

L'intérêt de ce type d'allocation est sa rapidité (pas de verrou posé lors de l'allocation, car pour une TLH il y a un seul thread qui peut y accéder alors que pour la Heap tous les threads peuvent y accéder).

II-B-2. Large Object Area allocation

Lorsque la JVM essaye d'allouer un gros objet (plus de 64 KB mais cela dépend de la version de la JVM) sans succès, car il n'y a plus de place dans la Small Object Area (SOA), l'allocation se fait directement dans une zone mémoire de la Tenured Space appelée Large Object Areas (LOA).

II-B-3. Heap lock allocation

Si l'allocation ne peut être satisfaite avec la Cache Allocation, elle se fait dans la Heap en posant un verrou pour éviter la concurrence entre tous les threads.

II-C. Gestions des objets par le GC

Maintenant que nous avons vu comment est architecturée la mémoire de la JVM et comment les objets sont alloués, regardons comment ils sont gérés par le Garbage Collector (GC).

II-C-1. Étapes possibles lors d'un GC

Afin de bien comprendre le fonctionnement du GC, regardons les étapes qui le composent.

II-C-1-a. Mark phase

La première étape consiste à marquer tous les objets vivants. Les objets non marqués seront considérés comme morts et éligibles au GC.

Image non disponible

Cette étape peut être réalisée de deux façons.

II-C-1-a-i. Parallel mark

Plusieurs threads (que l'on peut définir avec l'option -Xgcthreads) appelés helper/GC threads sont utilisés en parallèle des threads user/applicatifs qui exécutent le code applicatif.

Image non disponible
II-C-1-a-ii. Concurrent mark

Les threads de l'application sont utilisés pour faire l'étape Mark pendant qu'ils exécutent du code applicatif (et donc la tâche Mark est en concurrence avec l'exécution du code applicatif pour l'utilisation des threads).

Image non disponible

II-C-1-a-iii. Sweep phase

L'étape de sweep libère l'espace inutilisé.

Image non disponible

Elle peut être réalisée en parallèle (Parallel bitwise sweep) ou en concurrence (Concurrent sweep) de la même manière que pour l'étape Mark.

II-C-1-a-iii-i. Compaction phase

L'étape compaction défragmente la mémoire en déplaçant les objets en mémoire les uns à côté des autres.

Image non disponible

On peut la désactiver avec l'option -Xnocompactgc.

II-C-1-a-iii-i-i. Heap expansion phase

Lorsque la taille minimum de la Heap n'est pas la même que la taille maximum, il peut être nécessaire d'augmenter la taille totale de la Heap si la JVM a besoin de plus de mémoire. Cette étape s'appelle Heap expansion.

Image non disponible

II-C-1-a-iii-i-i-i. Heap shrinkage phase

De même que précédemment, la JVM peut décider de réduire la taille de sa Heap en faisant un Heap shrinkage.

Image non disponible

II-C-1-a-iii-i-i-i-i. Copy

Dans certains cas (par exemple dans la Scavenge GC ou dans la promotion d'objets), il peut être nécessaire de copier des objets d'une zone mémoire à une autre.

Image non disponible

II-D. Type de GC

Le type de GC utilisé dépend des zones mémoires sur lequel il va opérer.

II-D-1. Scavenge GC

C'est l'équivalent du Minor GC de la JVM Oracle Hotspot (anciennement SUN). Le périmètre du GC est la zone mémoire Nursery Space (Allocate Space + Survivor Space) et est exécuté lorsqu'il n'y a plus assez de place sur l'Allocate Space. Lorsqu'il n'y a plus assez de place dans la zone mémoire Allocate Space, un Scavenge GC est déclenché.

Image non disponible

Une copie des objets vivants est faite dans la Survivor Space et/ou dans le Tenured Space (s'ils ont atteint leur tenure age).

Image non disponible
Image non disponible

Les rôles entre l'Allocate Space et la Survivor Space sont inversés.

Image non disponible

II-D-2. Stop the world GC

C'est l'équivalent du Full GC de la JVM Hotspot et son périmètre est toute la Heap. Il est appelé Stop The World, car tous les threads applicatifs sont stoppés afin de libérer la mémoire.

Image non disponible

II-E. GC stratégie

Il existe plusieurs stratégies pour l'exécution du GC que l'on peut choisir avec l'option -Xgcpolicy:

II-E-1. Parallel mark-sweep-compact collector : optthruput

La formule de cette stratégie est :

optthruput = Continous Heap + Parallel mark + Parallel bitwise sweep + Compaction phase (optionnel) + Stop the world GC

Son déroulement est le suivant. Les objets sont alloués dans la Continous Heap jusqu'à ce qu'il n'y ait plus de places pour les allocations futures.

Image non disponible

L'étape Mark est lancée pour marquer les objets vivants.

Image non disponible

Puis l'étape Sweep.

Image non disponible

Si nécessaire, l'étape Compaction est exécutée.

Image non disponible

Puis cela recommence.

Attention aux temps de pause des GC qui peuvent être plus longs que pour les autres stratégies. Mais en contrepartie, le throughput est généralement plus important.

II-E-2. Concurrent collector : optavgpause

La formule de cette stratégie est :

optavgpause = Continous Heap + Concurrent mark + Concurrent sweep + Compaction phase (optionnel) + Stop the world GC

Son déroulement est le même que pour la stratégie optthruput sauf que les étapes Mark et Sweep seront faites de manière concurrente.

II-E-3. Generational collector : gencon

La formule de cette stratégie est :

optavgpause = Generational Heap + Concurrent mark + Parallel sweep + Compaction phase (optionnel) + Stop the world GC + Scavenge GC

C'est la stratégie par défaut depuis WebSphere Application Server V8.0.

Les objets sont alloués dans la zone Allocate Survivor.

Image non disponible

Lorsqu'il n'y a plus assez de place dans la zone Allocate Survivor, un Scavenge GC est exécuté.

Image non disponible
Image non disponible
Image non disponible

Au bout d'un certain temps, certains vieux « objets » sont promus dans la zone Tenured Space lors d'un Scavenge GC.

Image non disponible
Image non disponible

Et ainsi de suite jusqu'à ce qu'il n'y ait plus assez de place dans la zone Tenured Space. À ce moment, un GC est exécuté sur toute la Heap.

III. Conclusion

Comme nous l'avons vu de manière simplifiée, la JVM est assez complexe et la compréhension de son fonctionnement est utile lorsque nous développons et/ou optimisons une application. Or la JVM J9 d'IBM a quelques particularités par rapport à celle d'Oracle qu'il est bon de connaître afin d'en tirer le maximum. Par exemple, si vous avez des problèmes de performance et une version de WebSphere antérieure à la version 8, je vous conseille de regarder la stratégie GC sélectionnée.

IV. Remerciements

Je remercie Aliecom et en particulier Stéphane pour sa relecture.

Je remercie f-leb pour sa relecture orthographique.