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 \x{0096}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é.
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
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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).
II-C-1-a-iii. Sweep phase▲
L'étape de sweep libère l'espace inutilisé.
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.
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.
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.
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.
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é.
Une copie des objets vivants est faite dans la Survivor Space et/ou dans le Tenured Space (s'ils ont atteint leur tenure age).
Les rôles entre l'Allocate Space et la Survivor Space sont inversés.
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.
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.
L'étape Mark est lancée pour marquer les objets vivants.
Puis l'étape Sweep.
Si nécessaire, l'étape Compaction est exécutée.
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.
Lorsqu'il n'y a plus assez de place dans la zone Allocate Survivor, un Scavenge GC est exécuté.
Au bout d'un certain temps, certains vieux « objets » sont promus dans la zone Tenured Space lors d'un Scavenge GC.
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.