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 -clstatsAttaching 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.
$ jcmdhelp 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 :
$ jcmdGC.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.