BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Où est passée la PermGen Java ?

Où est passée la PermGen Java ?

Retrouvez cet article dans notre eMag InfoQ FR consacré à Java 8.

 

La Machine Virtuelle Java (JVM) utilise une représentation interne de ses classes contenant des métadonnées par classe, telles que les informations sur la hiérarchie des classes, les données de méthode (comme le bytecode, la pile et les tailles des variables), la réserve des constantes d'exécution, ainsi que les références symboliques résolues et les VTables.

Dans le passé (lorsque les chargeurs de classes personnalisés n'étaient pas répandus), les classes étaient pour la plupart "statiques" et étaient rarement déchargées ou collectées, et étaient donc étiquetés "permanentes". En outre, puisque les classes sont une partie de la mise en œuvre de la JVM et non pas créées par l'application, elles sont considérées comme de la mémoire "hors-tas".

Pour la JVM HotSpot avant le JDK 8, ces représentations permanentes vivaient dans une zone appelée la "génération permanente". Cette génération permanente était contiguë à la pile Java et était limitée à -XX: MaxPermSize, qui devait être indiqué sur la ligne de commande avant de démarrer la machine virtuelle Java ou qui était par défaut à 64M (85M pour les pointeurs 64 bits mis à l'échelle). Le recouvrement de la génération permanente était lié à la collecte de l'ancienne génération, de sorte que chaque fois que l'une ou l'autre était pleine, la génération permanente et l'ancienne génération étaient recouvrées. Un des problèmes évidents que vous pouvez détecter immédiatement est la dépendance sur -XX: MaxPermSize. Si la taille des métadonnées des classes va au-delà des limites de -XX: MaxPermSize, votre application sera à court de mémoire et vous rencontrerez une erreur OOM (Out Of Memory).

Anecdote : Avant le JDK7, pour la JVM HotSpot, les chaînes de caractères internées étaient également conservées au sein de la génération permanente, c-à-d. la PermGen, causant un grand nombre de problèmes de performance et des erreurs OOM. Pour plus d'informations sur la suppression des chaînes internées de la PermGen, vous pouvez consulter cette page.

Au Revoir PermGen, Bonjour Méta-Espace !

Avec l'avènement du JDK 8, nous n'avons plus de PermGen. Non, les informations de métadonnées n'ont pas disparu, mais simplement l'espace où elles sont stockées n'est plus contigu à la pile Java. Les métadonnées ont maintenant déménagé vers la mémoire native dans une région connue comme le "Méta-Espace".

Le passage au Méta-Espace était nécessaire puisque la PermGen était vraiment difficile à régler. Il y avait une possibilité que les métadonnées puissent se déplacer avec chaque passage complet du ramasse-miettes. En outre, il était difficile de fixer la taille de la PermGen car elle dépendait de beaucoup de facteurs tels que le nombre total de classes, la taille de la réserve des constantes, la taille de méthodes, etc.

En outre, chaque ramasse-miettes dans la HotSpot nécessitait du code spécialisé pour traiter les métadonnées dans la PermGen. Le détachement des métadonnées de la PermGen permet non seulement la gestion transparente du Méta-Espace, mais également des améliorations telles que la simplification de l'exécution complète du ramasse-miettes et la future dé-affectation concurrente des métadonnées de classe.

Qu'est-ce que la Suppression de l'Espace Permanent signifie pour l'Utilisateur Final ?

Comme les métadonnées de classes sont allouées depuis la mémoire native, l'espace disponible maximum est la mémoire totale disponible du système. Ainsi, vous ne rencontrerez plus d'erreurs OOM et pourriez finir par déborder dans l'espace de swap. L'utilisateur final peut choisir, soit de plafonner l'espace natif disponible maximum pour les métadonnées de classe, soit laisser la JVM agrandir la mémoire native afin d'héberger celles-ci.

Remarque : La suppression de la PermGen ne signifie pas que vos problèmes de fuite de chargeurs de classe sont résolus. Donc, oui, vous aurez toujours à surveiller votre consommation et planifier en conséquence, car une fuite finirait par consommer toute la mémoire native et causer un swap qui ne pourrait qu'empirer.

Passer au Meta-Espace et sa Répartition :

La VM Méta-Espace emploie maintenant des techniques de gestion de la mémoire pour gérer le Méta-Espace. De fait, déplacer les tâches depuis les différents ramasse-miettes à une seule VM Méta-Espace qui effectue toutes ses tâches en C ++ dans le Méta-Espace. Une intention derrière le Méta-Espace est tout simplement que la durée de vie des classes et de leurs métadonnées correspond à la durée de vie des chargeurs de classes. Autrement dit, tant que le chargeur de classe est vivant, les métadonnées restent vivantes dans le Méta-Espace et ne peuvent pas être libérées.

Nous avons utilisé le terme Méta-Espace de manière approximative. Plus formellement, chaque zone de stockage de chargeurs de classes est appelée "un méta-espace". Et ces méta-espaces sont collectivement appelés "le Méta-Espace". La récupération des méta-espaces par chargeur de classes peut survenir uniquement après que son chargeur de classe ne soit plus vivant et ait été déclaré mort par le ramasse-miettes. Il n'y a aucun transfert ou compactage dans ces méta-espaces. Mais les métadonnées sont scannées pour des références Java.

La VM Méta-Espace gère l'allocation de Méta-Espace en employant un allocateur par blocs. La taille de bloc dépend du type de chargeur de classes. Il y a une liste globale de fragments. Chaque fois qu'un chargeur de classes a besoin d'un fragment, il le prend depuis cette liste globale et gère sa propre liste de fragments. Quand un chargeur de classes meurt, ses fragments sont libérés et retournés à la liste globale. Les fragments sont ensuite divisés en blocs et chaque bloc contient une unité de métadonnées. L'attribution de blocs de fragments est linéaire (bump de pointeur). Les fragments sont placés en dehors des espaces mappés à la mémoire (mmapped). Il existe une liste liée de ces espaces globaux virtuels mmappés et chaque fois que l'espace virtuel est vidé, il est retourné au système d'exploitation.

La figure ci-dessus montre la répartition du Méta-Espace avec des méta-fragments dans des espaces virtuels mmapped. Les chargeurs de classes 1 et 3 illustrent la réflexion ou des chargeurs anonymes, et ils emploient un taille "spécialisée" de fragment. Les chargeurs de classes 2 et 4 peuvent employer une taille de bloc petite ou moyenne en fonction du nombre d'éléments dans ces chargeurs.

Réglages et Outils pour le Méta-Espace

Comme mentionné précédemment, une VM Méta-Espace va gérer la croissance du Méta-Espace. Mais il peut y avoir des scénarios où vous voudrez peut-être limiter la croissance en spécifiant explicitement -XX: MaxMetaspaceSize sur la ligne de commande. Par défaut, -XX: MaxMetaspaceSize n'a pas de limite, donc, techniquement, la taille du Méta-Espace pourrait croître dans l'espace de swap et vous commenceriez à obtenir des échecs d'allocation native.

Pour une JVM de classe de serveur 64 bits, la valeur initiale/par défaut de -XX: MetaspaceSize est de 21 Mo. Ceci est la limite supérieure initiale. Une fois cette limite atteinte, une exécution complète du ramasse-miettes est déclenchée pour décharger les classes (quand son chargeur de classes n'est plus en vie) et la limite supérieure est réinitialisée. La nouvelle valeur de la limite haute dépend de la quantité de Méta-Espace libérée. Si un espace insuffisant est libéré, la limite supérieure augmente ; si trop d'espace est libéré, la limite supérieure descend. Ceci sera répété plusieurs fois si la limite initiale est trop faible. Et vous serez en mesure de visualiser les passages répétés du ramasse-miettes dans vos journaux. Dans un tel scénario, il est conseillé de régler le -XX: MetaspaceSize à une valeur plus élevée sur la ligne de commande afin d'éviter les exécutions initiales du ramasse-miettes.

Après les exécutions ultérieures, la VM Méta-Espace va automatiquement ajuster votre limite supérieure, de manière à repousser la prochaine exécution du ramasse-miettes Méta-Espace.

Il existe également deux options: -XX: MinMetaspaceFreeRatio et -XX: MaxMetaspaceFreeRatio. Ceux-ci sont analogues aux paramètres GC FreeRatio et ils peuvent également être définis sur la ligne de commande.

Quelques outils ont été modifiés pour obtenir plus d'informations concernant le Méta-Espace et ils sont énumérés ici :

  • jmap -clstats  : Imprime les statistiques des chargeurs de classes (ceci remplace désormais -permstat qui était utilisé pour imprimer les statistiques des chargeurs de classes pour les JVMs avant le JDK 8). Un exemple de sortie lors de l'exécution du benchmark Avrora de DaCapo :
$ jmap -clstats 
Attaching to process ID 6476, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.5-b02
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness.liveness analysis may be inaccurate ...
class_loader classes      bytes parent_loader     alive? type 

     655  1222734     null      live   
0x000000074004a6c0    0    0    0x000000074004a708    dead      java/util/ResourceBundle$RBClassLoader@0x00000007c0053e20
0x000000074004a760    0    0      null      dead      sun/misc/Launcher$ExtClassLoader@0x00000007c002d248
0x00000007401189c8     1     1471 0x00000007400752f8    dead      sun/reflect/DelegatingClassLoader@0x00000007c0009870
0x000000074004a708    116   316053    0x000000074004a760   dead      sun/misc/Launcher$AppClassLoader@0x00000007c0038190
0x00000007400752f8    538  773854    0x000000074004a708   dead      org/dacapo/harness/DacapoClassLoader@0x00000007c00638b0
total = 6      1310   2314112           N/A       alive=1, dead=5     N/A    
  • jstat –gc  : Imprime maintenant l'information de Méta-Espace comme illustré dans l'exemple suivant :

  • jcmd GC.class_stats : Il s'agit d'une nouvelle commande de diagnostic qui permet à l'utilisateur final de se connecter à une JVM en direct et de récupérer un histogramme détaillé des métadonnées des classes Java.

    Note : Avec le JDK 8 build 13, vous démarrez Java avec ‑XX:+UnlockDiagnosticVMOptions.

$ jcmd  help GC.class_stats
9522:
GC.class_stats
Provide statistics about Java class meta data. Requires -XX:+UnlockDiagnosticVMOptions.

Impact: High: Depends on Java heap size and content. 

Syntax : GC.class_stats [options] [] 

Arguments:
     columns : [optional] Comma-separated list of all the columns to show. If not specified, the following columns are shown: InstBytes,KlassBytes,CpAll,annotations,MethodCount,Bytecodes,MethodAll,ROAll,RWAll,Total (STRING, no default value)

Options: (options must be specified using the  or = syntax)
     -all : [optional] Show all columns (BOOLEAN, false)
     -csv : [optional] Print in CSV (comma-separated values) format for spreadsheets (BOOLEAN, false)
     -help : [optional] Show meaning of all the columns (BOOLEAN, false)

Note : Pour plus d'informations sur les colonnes, vous pouvez vous rendre ici.

Un exemple de sortie :

 $ jcmd  GC.class_stats 

7140:
Index Super InstBytes KlassBytes annotations   CpAll MethodCount Bytecodes MethodAll   ROAll   RWAll   Total ClassName
    1    -1    426416        480           0       0           0         0         0      24     576     600 [C
    2    -1    290136        480           0       0           0         0         0      40     576     616 [Lavrora.arch.legacy.LegacyInstr;
    3    -1    269840        480           0       0           0         0         0      24     576     600 [B
    4    43    137856        648           0   19248         129      4886     25288   16368   30568   46936 java.lang.Class
    5    43    136968        624           0    8760          94      4570     33616   12072   32000   44072 java.lang.String
    6    43     75872        560           0    1296           7       149      1400     880    2680    3560 java.util.HashMap$Node
    7   836     57408        608           0     720           3        69      1480     528    2488    3016 avrora.sim.util.MulticastFSMProbe
    8    43     55488        504           0     680           1        31       440     280    1536    1816 avrora.sim.FiniteStateMachine$State
    9    -1     53712        480           0       0           0         0         0      24     576     600 [Ljava.lang.Object;
   10    -1     49424        480           0       0           0         0         0      24     576     600 [I
   11    -1     49248        480           0       0           0         0         0      24     576     600 [Lavrora.sim.platform.ExternalFlash$Page;
   12    -1     24400        480           0       0           0         0         0      32     576     608 [Ljava.util.HashMap$Node;
   13   394     21408        520           0     600           3        33      1216     432    2080    2512 avrora.sim.AtmelInterpreter$IORegBehavior
   14   727     19800        672           0     968           4        71      1240     664    2472    3136 avrora.arch.legacy.LegacyInstr$MOVW
…
1299  1300         0        608           0     256           1         5       152     104    1024    1128 sun.util.resources.LocaleNamesBundle
 1300  1098         0        608           0    1744          10       290      1808    1176    3208    4384 sun.util.resources.OpenListResourceBundle
 1301  1098         0        616           0    2184          12       395      2200    1480    3800    5280 sun.util.resources.ParallelListResourceBundle
              2244312     794288        2024 2260976       12801    561882   3135144 1906688 4684704 6591392 Total
                34.0%      12.1%        0.0%   34.3%           -      8.5%     47.6%   28.9%   71.1%  100.0%
Index Super InstBytes KlassBytes annotations   CpAll MethodCount Bytecodes MethodAll   ROAll   RWAll   Total ClassName

Questions d'Actualité :

Comme mentionné précédemment, la VM Méta-Espace emploie un allocateur de fragments. Il existe plusieurs tailles de fragments en fonction du type de chargeur de classes. En outre, les éléments de classe eux-mêmes ne sont pas de taille fixe, donc il y a des chances que les fragments libres puissent ne pas être de la même taille que le fragment nécessaire pour un élément de classe. Tout cela pourrait conduire à une fragmentation. La VM Méta-Espace n'emploie pas (encore) le compactage, donc la fragmentation est une préoccupation majeure à l'heure actuelle.

A Propos de l'Auteure

 Monica Beckwith est Consultante en Performance Java. Ses expériences passées incluent la collaboration avec Oracle / Sun et AMD et l'optimisation de la JVM pour les systèmes de classe serveur. Monica a été élue JavaOne Rock Star 2013 et a été la Performance Lead pour les Ramasse-Miettes Garbage First (G1 GC). Vous pouvez suivre Monica sur Twitter mon_beck.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT