Développons en Java
v 2.40   Copyright (C) 1999-2023 .   
65. La gestion de la mémoire dans la JVM HotSpot 67. La décompilation et l'obfuscation Imprimer Index Index avec sommaire Télécharger le PDF

 

66. La JVM HotSpot dans un conteneur Docker

 

chapitre    6 6

 

Niveau : niveau 5 Confirmé 

 

L'utilisation des conteneurs s'est généralisée notamment avec l'accroissement de l'utilisation du cloud pour le déploiement et l'exécution d'applications.

Il est donc courant de devoir conteneuriser une application Java exécutée dans une JVM.

Sous Linux, plusieurs gestionnaires de conteneurs sont disponibles :

Docker est historiquement le plus utilisé.

Généralement, surtout lors de l'utilisation d'orchestrateur comme Kubernetes, il est courant de limiter les ressources du conteneur en termes de CPU et de mémoire. Ceci facilite les capacités d'orchestration automatisé mais aussi limite la possibilité d'un processus exécuté dans un conteneur à consommer toutes les ressources sur le noeud dans lequel il s'exécute.

La limitation des ressources allouées à l'environnement d'exécution d'une JVM peut avoir des conséquences notamment sur ses performances car elle est historiquement créée et utilisée sur des serveurs avec des ressources plus ou moins importantes.

La machine virtuelle Java, jusqu'à la version du JDK 10, n'est pas pleinement consciente des mécanismes d'isolation utilisés par les conteneurs, ce qui peut conduire à un comportement inattendu entre différents environnements. 

Ce chapitre contient plusieurs sections :

 

66.1. La limitation des ressources d'un conteneur Docker

La popularité des conteneurs est en partie due aux facilités de mise en ouvre des fonctionnalités sous-jacentes. Ces fonctionnalités sont proposées par le noyau Linux : ce sont notamment cgroups et namespaces.

Les conteneurs mettent en ouvre des mécanismes d'isolation où les ressources (CPU, mémoire, système de fichiers, réseau, etc.) d'un conteneur sont isolées les unes des autres. Cette isolation est possible grâce à une fonctionnalité du noyau Linux nommée namespaces.

Les namespaces sont essentiellement utilisés pour offrir une des vues isolées du système. Ainsi, chaque conteneur peut voir sa propre vue sur différents namespaces :

Ainsi, chaque conteneur possède un processus avec l'ID 1.

Les control groups (cgroups en abrégé) sont utilisés pour mesurer et limiter l'accès aux ressources.

Il existe deux versions majeures de cgroups :

Docker propose ainsi de limiter les ressources utilisables par le conteneur : il est possible de limiter les ressources mémoire et CPU utilisables par un conteneur Docker. Ces fonctionnalités sont intéressantes lors de l'exécution de nombreux conteneurs dans un ensemble de serveurs orchestré par Kubernetes. Kubernetes tente de rationnaliser le déploiement des conteneurs sur les serveurs notamment en tenant compte des ressources utilisables par les conteneurs.

 

66.1.1. La limitation en mémoire

Certaines commandes comme top ou free ainsi que la JVM pour les versions antérieures à Java 8u131 et Java 10 ne prennent pas en compte les restrictions d'utilisation de ressources pouvant être définies avec cgroups.

Exemple :
$ docker run -it ubuntu free -h
              total        used        free      shared  buff/cache   available
Mem:           2.0G         95M        1.5G        229M        345M        1.6G
Swap:          1.4G          0B        1.4G
 
$ docker run -it -m128M --memory-swap=128M ubuntu free -h
              total        used        free      shared  buff/cache   available
Mem:           2.0G         96M        1.5G        229M        346M        1.6G
Swap:          1.4G          0B        1.4G

Les options --memory et --memory-swap de Docker permettent de définir les limitations de ressources en mémoire utilisables par les processus exécutés dans le conteneur.

La documentation de Docker précise que ces deux options peuvent être utilisées pour limiter la quantité de mémoire utilisable par un conteneur :

La mémoire swap est utilisable si celle-ci est supporté par le système hôte.

Il y a plusieurs possibilités concernant la quantité de mémoire utilisable par un conteneur :

Si --memory-swap est précisée alors sa valeur doit toujours être supérieure à celle de l'option --memory car malgré son nom elle précise la quantité totale de mémoire incluant le swap.

Exemple :
$ docker run --rm --memory=256m --memory-swap=128m -it ubuntu /bin/bash
C:\Program Files\Docker Toolbox\docker.exe: Error response from daemon: Minimum memoryswap limit
should be larger than memory limit, see usage.
See 'C:\Program Files\Docker Toolbox\docker.exe run --help'.

Si la limite de consommation mémoire est atteinte, alors le noyau Linux tue le processus.

Par exemple, en lançant un conteneur sur l'image jboss/wildfly en limitant la mémoire du conteneur à 64Mo. Le serveur Wildfly démarre mais au bon d'un certain temps, le conteneur est arrêté avec le message « *** JBossAS process (80) received KILL signal *** » dans la sortie standard.

Exemple :
$ docker run -it --name wildfly -m=64M jboss/wildfly
=========================================================================
 
  JBoss Bootstrap Environment
 
  JBOSS_HOME: /opt/jboss/wildfly
 
  JAVA: /usr/lib/jvm/java/bin/java
 
  JAVA_OPTS:  -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m
  -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman 
  -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED 
  --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED 
  --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED
 
=========================================================================
 
13:28:54,179 INFO  [org.jboss.modules] (main) JBoss Modules version 1.9.1.Final
13:29:08,588 INFO  [org.jboss.msc] (main) JBoss MSC version 1.4.8.Final
13:29:08,887 INFO  [org.jboss.threads] (main) JBoss Threads version 2.3.3.Final
13:29:14,499 INFO  [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: WildFly
 Full 17.0.1.Final (WildFly Core 9.0.2.Final) starting
*** JBossAS process (80) received KILL signal ***
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS
              PORTS               NAMES
 
$

Il est possible de regarder les informations relatives au conteneur et notamment l'objet Json State :

Exemple :
$ docker inspect wildfly
[
    {
        "Id": "179552e4dc026789f6546fc5b04b5375808841ce8990f1a9cd031fbbdad10f44",
        "Created": "2019-08-14T13:28:35.900271741Z",
        "Path": "/opt/jboss/wildfly/bin/standalone.sh",
        "Args": [
            "-b",
            "0.0.0.0"
        ],
        "State": {
            "Status": "exited",
            "Running": false,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": true,
            "Dead": false,
            "Pid": 0,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2019-08-27T09:15:14.831865506Z",
            "FinishedAt": "2019-08-27T09:47:02.238997935Z"
        },

La propriété OOMKilled est à true, ce qui signifie que le démon a tué le conteneur par manque de ressource mémoire.

 

66.1.2. La limitation en CPU

Par défaut, un processus exécuté dans un conteneurisé peut utiliser toute la CPU disponible sur le système l'hôte. Cependant, il est possible de limiter la consommation de ressource CPU par un conteneur Docker de plusieurs manières.

Cela peut être utile notamment :

L'utilisation de ces fonctionnalités pour limiter la consommation CPU a une incidence sur la valeur retournée par la méthode availableProcessors() de la classe Runtime selon la version de Java utilisée.

Dans un conteneur Linux, il est possible de limiter les ressources CPU de plusieurs manières :

La prise en charge de ces fonctionnalités dépend de la version de la JVM, notamment depuis l'ajout de l'option ContainerSupport qui améliore le support de la prise en compte des restrictions de ressources CPU définies sur un conteneur.

La JVM de Java 10 divise les cpu_shares par 1024 pour déterminer le nombre de coeurs utilisables.

La JVM de Java 11 regarde d'abord cpu_quota, si celui-ci est défini, il est divisé par cpu_period pour obtenir un nombre effectif de coeurs. Si ce n'est pas le cas, les cpu_shares sont utilisés comme le fait le JDK 10.

La limitation de la CPU pour un conteneur qui exécute une JVM doit être soigneusement évalué selon l'application.

La prise en compte de ses restrictions par la JVM est importante pour éviter qu'une JVM n'utilise trop de threads pour son ramasse-miette ou des pools de threads notamment celui du framefork Fork/Join. C'est d'autant plus important pour ne pas saturer la CPU d'un hôte sur lequel s'exécute de nombreux conteneurs.

 

66.1.2.1. CPU Shares

CPU shares définit une pondération de priorité sur tous les cycles du CPU à travers tous les cours. Le nombre de cycles dépend de l'ensemble des processus qui s'exécutent sur le noeud.

Cela rationne l'utilisation de la CPU par le processus du conteneur selon la proportion précisée par rapport aux autres conteneurs mais uniquement lorsque le système est sous forte charge. Si la CPU est libre alors le processus peut dépasser la limite fixée.

Par défaut, tous les conteneurs ont la même proportion de cycles CPU. Cette proportion peut être modifiée en changeant la pondération de la part de la CPU du conteneur par rapport à celle de tous les autres conteneurs en cours d'exécution sur le système hôte.

La contrainte de type CPU shares est précisée en utilisation l'option --cpu-shares ou -c.

La valeur par défaut est 1024. L'argument précisé est interprété par rapport à tous les autres conteneurs du système, ce qui rend difficile l'interprétation concrète de la valeur.

Par exemple, avec trois conteneurs, l'un a cpu-shares à 1024 et les deux autres ont un cpu-shares à 512. Lorsque les processus des trois conteneurs tentent d'utiliser 100% de la CPU, le premier conteneur reçoit 50% du temps CPU total et les deux autres 25%. Si on ajoute un quatrième conteneur avec un cpu-shares à 1024, le premier et le quatrième conteneur obtiennent 33% du temps CPU. Les deux autres conteneurs restants reçoivent 16,5% de la CPU.

La proportion ne s'applique que lorsque des processus gourmands en ressources CPU sont en cours d'exécution. Lorsque les tâches d'un conteneur sont inactives, les autres conteneurs peuvent utiliser le temps CPU restant. La quantité réelle de temps processeur varie donc en fonction des autres conteneurs en cours d'exécution sur le système.

Sur un système multicoeurs, les parts de temps CPU sont réparties sur tous les cours de la CPU. Même si un conteneur est limité à moins de 100% du temps processeur, il peut utiliser 100% de chaque cour de processeur.

Exemple :
$ docker run -it --rm --cpu-shares=1024 openjdk:10.0.2-jdk
Aug 25, 2019 7:54:09 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
2
 
jshell> /ex
|  Goodbye
 
$ docker run -it --rm --cpu-shares=512 openjdk:10.0.2-jdk
Aug 25, 2019 7:54:57 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
1
 
$ docker run -it --rm --cpu-shares=4096 openjdk:10.0.2-jdk
Aug 25, 2019 7:55:41 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
2

 

66.1.2.2. CPU Period/Quota

Cette contrainte utilise le Linux Completely Fair Scheduler pour restreindre l'utilisation de la CPU par le conteneur. Le conteneur aura un temps CPU limité même lorsque la machine est peu chargée et que la charge de travail peut être répartie sur tous les processeurs de l'hôte.

La contrainte de type CPU period/quota est précisée en utilisation l'option --cpus ou les options --cpu-period et --cpu-quota (cpus = cpu-quota / cpu-period).

La période par défaut de CPU CFS est 100ms. Cette valeur peut être modifiée grâce à l'option --cpu-period. Cette option s'utilise en conjonction de l'option --cpu-quota

De manière plus simple, il est possible d'utiliser l'option --cpus avec une valeur flottante. Par exemple, la valeur 0.5 correspond à 50% de CPU.

La valeur par défaut est 0 qui implique aucune restriction.

Exemple :
$ docker run -it --rm --cpus=1 openjdk:10.0.2-jdk
Aug 25, 2019 8:54:02 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
1
 
jshell> /ex
|  Goodbye
 
$ docker run -it --rm --cpus=0.5 openjdk:10.0.2-jdk
Aug 25, 2019 8:54:36 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
1
 
jshell> /ex
|  Goodbye
 
$ docker run -it --rm --cpus=2 openjdk:10.0.2-jdk
Aug 25, 2019 8:55:07 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
2
 
jshell> /ex
|  Goodbye

 

66.1.2.3. CPU Sets

La contrainte CPU set permet de limiter l'exécution des processus du conteneur uniquement sur les cours précisés. Cela peut être intéressant pour éviter que les processus ne changent de CPU ou profiter sur des systèmes NUMA où les CPU ont un accès rapide à différentes régions de la mémoire.

Contrairement aux deux contraintes précédentes, le processus exécuté dans le conteneur est rattaché à des cours spécifiques dédiés. Il est possible que le processus doive partager ces cours, mais ne pourra pas utiliser d'autres cours.

La contrainte de type CPU sets est précisée en utilisation l'option --cpuset-cpus.

Si un seul CPU est utilisé, le premier CPU dans l'exemple ci-dessous, alors le nombre de coeurs détecté par la JVM est bien 1 seul.

Exemple :
$ docker run -it --rm --cpuset-cpus="0" openjdk:10.0.2-jdk
Aug 29, 2019 8:53:50 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
1
 
jshell> /ex
|  Goodbye

Si plusieurs CPU sont précisés, les deux premiers CPU dans l'exemple ci-dessous, alors le nombre de coeurs détecté par la JVM est bien 2.

Exemple :
$ docker run -it --rm --cpuset-cpus="0-1" openjdk:10.0.2-jdk
Aug 29, 2019 8:56:55 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> System.out.println(Runtime.getRuntime().availableProcessors())
2
 
jshell> /ex
|  Goodbye

Il est aussi possible de préciser chaque CPU séparé par une virgule.

Exemple :
$ docker run -it --rm --cpuset-cpus="0,2,4" openjdk:10.0.2-jdk

 

66.1.2.4. Le choix du type de limitation

Sur une machine avec un nombre suffisant de coeurs, il peut être intéressant d'utiliser le CPU set cela devrait réduire les changements de contexte et de profiter mieux des caches CPU.

Il peut être intéressant d'utiliser CPU period/quota pour des applications qui requièrent de nombreux threads qui sont souvent en attente.

L'utilisation de CPU shares peut être intéressant si les applications peuvent partager des cycles de processeurs disponibles.

Dans tous les cas, pour valider son choix, il faut profiler et mesurer.

 

66.2. L'utilisation et la configuration des ressources utilisables par une JVM

Une JVM propose de très nombreuses options pour sa configuration. Pour permettre une utilisation simple, la JVM propose un mécanisme appelé ergonomics qui définit certaines valeurs par défaut en tenant compte de l'environnement d'exécution.

Ces valeurs par défaut peuvent être remplacées par des valeurs fournies explicitement.

 

66.2.1. Les ergonomics de la JVM

Lors de l'exécution d'une application sans configuration particulière de la JVM, cette dernière va déterminer des valeurs de paramètres par défaut. Certaines valeurs sont en dur et d'autres sont déterminées à partir d'informations extraites de l'environnement d'exécution. Ce mécanisme de la JVM est désigné par le terme ergonomics.

Les ergonomics sont des règles utilisées par la JVM pour définir certaines valeurs de configurations par défaut. Ils utilisent le nombre de CPU et la quantité de RAM pour définir certaines valeurs par défaut de plusieurs propriétés de la JVM si elles ne sont pas explicitement définies notamment :

Les valeurs définies via les ergonomics de la JVM dépendent de la version et du fournisseur. Pour éviter des surprises, il est préférable de définir explicitement ces valeurs.

Pour demander à la JVM d'afficher les valeurs de toutes ses propriétés, il faut utiliser l'option -XX:+PrintFlagsFinal

Exemple :
java -XX:+PrintFlagsFinal -version | grep -Ei "gcthreads|maxheap|maxram"
     uint ConcGCThreads                = 1           {product} {ergonomic}
    uintx MaxHeapFreeRatio             = 70          {manageable} {default}
   size_t MaxHeapSize                  = 2122317824  {product} {ergonomic}
 uint64_t MaxRAM                      = 137438953472 {pd product} {default}
    uintx MaxRAMFraction              = 4            {product} {default}
     uint ParallelGCThreads           = 4            {product} {default}
     bool UseDynamicNumberOfGCThreads = false        {product} {default}
java version "9.0.1"
Java(TM) SE Runtime Environment (build 9.0.1+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)

Attention : les valeurs peuvent et parfois changent selon la version, le fournisseur et le système d'exploitation utilisé.

 

66.2.2. L'utilisation du nombre de CPU par la JVM

Si les options -XX:ParallelGCThreads (nombre de threads pour les ramasse-miettes parallèles) ou -XX:CICompilerCount (nombre de threads pour le compilateur JIT) ne sont pas précisées, la JVM récupère le nombre de CPU du système hôte pour déterminer leur valeur.

Selon certaines versions de Java, la JVM va déterminer le nombre de CPU éventuellement en tenant compte des restrictions définies sur le conteneur si elles sont utilisées :

A partir de Java 11, si des limitations sur CPU sont appliquées sur le conteneur, la formule utilisée par la JVM pour déterminer le nombre de coeur utilisable par la JVM dans le conteneur est : min(cpuset-cpus, cpu-shares/1024, cpus) arrondi au nombre entier supérieur.

En se basant sur le nombre de coeurs disponible, la JVM détermine certaines valeurs par défaut si celles-ci ne sont pas explicitement définies, notamment :

De nombreux outils ou frameworks utilisent aussi la méthode availableProcessors() de la classe Runtime pour déterminer la taille de pool de threads (Netty, ElasticSearch, ...).

 

66.2.3. La gestion de la mémoire

L'empreinte mémoire d'une JVM est composée de différents éléments notamment :

Il est nécessaire de tenir compte de tout cela lors de la limitation de la mémoire d'un conteneur exécutant une application Java et ne pas uniquement prendre en compte du heap.

La quantité maximale de heap utilisable par JVM est soit définie explicitement avec l'option -Xmx soit déterminé par un mécanisme de la JVM appelé ergonomics.

Sans configuration explicite, les ergonomics d'une JVM définissent entre-autre la taille max du heap en fonction de la RAM totale de la machine sur laquelle elle s'exécute en appliquant la formule :

MaxHeapSize = TailleRAM / MaxRAMFraction

Avec par défaut, TailleRAM est la taille de la RAM de la machine hôte et MaxRAMFraction est par défaut à 4.

Exemple : sur une machine avec 8Go de RAM, la taille max du heap est fixée à 2Go soit ¼ de la taille de la RAM.

Exemple :
java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram"
   size_t MaxHeapSize            = 2122317824       {product} {ergonomic}
 uint64_t MaxRAM                 = 137438953472     {pd product} {default}
    uintx MaxRAMFraction         = 4                {product} {default}
java version "9.0.1"
Java(TM) SE Runtime Environment (build 9.0.1+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)

L'option la plus couramment utilisée pour gérer la mémoire de la JVM est de définir la taille maximale du heap en utilisant l'option -Xmx.

Exemple :
java -XX:+PrintFlagsFinal -Xmx1g -version | grep -Ei "maxheapsize|maxram"
   size_t MaxHeapSize          = 1073741824     {product} {command line}
 uint64_t MaxRAM               = 137438953472   {pd product} {default}
    uintx MaxRAMFraction       = 4              {product} {default}
java version "9.0.1"
Java(TM) SE Runtime Environment (build 9.0.1+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)

Une autre manière de contrôler la taille maximale du heap est de définir la valeur de la propriété MaxRAM.

Exemple :
java -XX:+PrintFlagsFinal -XX:MaxRAM=1g -version | grep -Ei "maxheapsize|maxram"
   size_t MaxHeapSize          = 268435456      {product} {ergonomic}
 uint64_t MaxRAM               = 1073741824     {pd product} {command line}
    uintx MaxRAMFraction       = 4              {product} {default}
java version "9.0.1"
Java(TM) SE Runtime Environment (build 9.0.1+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)

Dans ce cas, les ergonomics définissent la taille maximale du heap.

Lorsque la machine hôte possède au moins deux processeurs 64bits et 2Go de Ram, alors la JVM s'exécute en mode server. Dans le mode server, les ergonomics définissent la taille maximale du tas à un quart de la mémoire RAM totale de la machine. La valeur est cependant limitée à 32 go (la taille maximale adressable avec les compressed oops). Pour préciser une valeur supérieure, il faut utiliser l'option -Xmx.

Le heap est quasiment toujours la plus grande partie de mémoire utilisée par une JVM. Mais il est aussi important de prendre en compte que la mémoire utilisée par une JVM n'est pas que le heap.

La JVM a aussi besoin pour son fonctionnement interne d'une zone de mémoire : Permgen avant Java 8, remplacée par le MetaSpace à partir de Java 8. Elle contient notamment les classes chargées.

Il faut aussi tenir compte des piles de threads. Chaque thread possède une pile dont la taille par défaut est fixée par le paramètre -Xss. La valeur par défaut du paramètre -Xss dépend du système hôte et de la JVM utilisée. Une pile de thread contient les variables locales et les valeurs intermédiaires entre différents appels de méthodes. Même si une application ne créé pas elle-même de threads, la JVM en créé un certain nombre : un pour exécuter la méthode main(), éventuellement plusieurs pour le ramasse-miettes et le compilateur JIT, ...

Le code natif issu de la compilation du bytecode par le JIT est stocké dans une zone dédiée nommé Code Cache dont la taille est configurable en utilisant les options -XX:InitialCodeCacheSize et -XX:ReservedCodeCacheSize.

Une application peut allouer de la mémoire native en dehors du heap en utilisant les ByteBuffers de l'API NIO ou par malloc en utilisant JNI. La JVM peut aussi allouer de la mémoire native en plus du heap.

Il est possible d'activer le traçage de l'utilisation de mémoire native dans la JVM grâce à NMT (Native Memory Tracking) en utilisant l'option -XX:NativeMemoryTracking. Elle peut prendre trois valeurs :

Exemple :
$ java -XX:NativeMemoryTracking=summary -Xms256m -Xmx256m -jar monapp.jar

Une fois activée, il est possible d'utiliser la commande jcmd avec en paramètres le pid de la JVM concernée et la valeur VM.native_memory pour obtenir des informations instantanées de la part de NMT.

Exemple :
$ jcmd 6741 VM.native_memory

Il est aussi possible d'obtenir une évolution entre deux instants dans le temps en utilisant les options baseline puis summary.diff

Exemple :
$ jcmd 6741 VM.native_memory baseline
...
$ jcmd 6741 VM.native_memory summary.diff

Il est possible de limiter la quantité totale de mémoire utilisable par la JVM (pour toutes les zones de mémoire dont elle a besoin) en utilisant l'option -XX:MaxRAM.

 

66.3. Le support de Docker par la JVM

Avant Java 10, exécuter des applications Java dans des conteneurs Linux était un peu délicat et nécessitait une configuration particulière pour éviter des surprises. Ces problèmes n'affectent pas seulement les versions de Java antérieures à 10, mais aussi certains outils qui collectent des informations du système comme top ou free. C'est parce que ces outils et la JVM ont été implémentés avant l'utilisation de cgroups et qu'ils ne sont donc pas optimisés pour une exécution dans un conteneur.

Avant Java 10, la JVM ne proposait pas de support pour détecter qu'elle fonctionnait à l'intérieur d'un conteneur et donc que certaines ressources qui lui sont allouées sont limitées en mémoire et/ou CPU. C'est la raison pour laquelle, il ne faut pas laisser les ergonomics de la JVM déterminer certaines valeurs par défaut notamment celles qui utilisent la quantité de mémoire et le nombre de CPU.

Historiquement, pour pallier une partie de ces problèmes, il était possible d'utiliser l'image de Fabric8.

A partir de Java 9 et Java 8u131, il est possible d'utiliser l'option expérimentale -XX:+UseCGroupMemoryLimitForHeap pour demander à la JVM de tenir compte des limites de mémoire définies par cgroups.

Java 10 a apporté plusieurs améliorations dans le support de l'exécution d'une JVM dans un conteneur.

Dans tous les cas, il est nécessaire de faire des tests.

 

66.3.1. Java SE 8 < u131

Lors de l'utilisation d'une version antérieure de Java à 8u131 dans un conteneur, laisser la JVM déterminer ces valeurs par défaut via ses ergonomics peut entraîner un comportement différent de celui lors de son exécution sur le système hôte. Spécifier la quantité de mémoire disponible pour un conteneur n'affecte pas ce que la JVM croit être disponible.

A moins de préciser explicitement la taille maximale du heap, la JVM détermine celle-ci en fonction de la RAM de l'hôte sur lequel elle s'exécute. Dans une JVM en mode server, la taille par défaut est un quart de la taille totale de la RAM de l'hôte.

Par défaut, si elle n'est pas précisée avec l'option -Xmx, la JVM de Java 8 utilise un quart de la mémoire totale du système hôte comme taille maximale du heap. La valeur de la taille mémoire est extraite du système hôte même si la JVM est exécutée dans un conteneur.

Exemple sur une machine hôte avec 8Go de Ram

Exemple :
$ java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
    uintx MaxHeapSize                          := 2122317824                      {product}

La taille max du heap déterminée par la JVM est ¼ de 8go soit 2Go.

Exemple dans une VM avec 1Go de Ram

Exemple :
$ docker container run -it openjdk:8u121-jre-alpine java -XX:+PrintFlagsFinal -version |
 grep MaxHeapSize
    uintx MaxHeapSize                          := 260046848                       {product}

La taille max du heap déterminée par la JVM est ¼ de 1Go soit 256Mo.

Si la mémoire utilisable par le conteneur est limitée par configuration, alors la valeur extraite du système ne correspond pas à celle allouée au conteneur.

Exemple :
$ docker container run -it -m=128M openjdk:8u121-jre-alpine java -XX:+PrintFlagsFinal -version
| grep MaxHeapS ize
    uintx MaxHeapSize         := 524288000         {product}
 
$ docker container run -it -m=256M openjdk:8u121-jre-alpine java -XX:+PrintFlagsFinal -version 
| grep MaxHeapSize
    uintx MaxHeapSize         := 524288000         {product}

La valeur fournie à l'option -m de Docker est utilisée comme valeur maximale de la mémoire utilisable ainsi que la valeur de l'espace de swap. Ainsi dans le conteneur, seul le double de la valeur précisée pourra être utilisée par la JVM. Au-delà de cette valeur, la JVM tente d'agrandir le heap jusqu'à dépasser la limite indiquée au conteneur. Le démon Docker va alors tuer le conteneur.

Par exemple, si le conteneur est exécuté sur une machine hôte possédant 32 Go de mémoire. La taille de la mémoire utilisable par le conteneur est limitée à 1Go. Si l'option -Xmx n'est pas utilisée pour la JVM, celle-ci va utiliser les valeurs par défaut déterminées par les ergonomics.

La JVM récupère la mémoire totale de la machine hôte. La JVM ne sachant pas détecter qu'elle s'exécute dans un conteneur, elle pense qu'elle s'exécute sur la machine hôte et donc que la mémoire disponible est de 32 Go. Par défaut, la JVM utilisera, comme taille maximale de son heap, le quart de 32Go soit 8Go comme valeur du paramètre -Xmx.

Au fur et à mesure de son utilisation, la taille du heap grossit jusqu'à dépasser 1Go. Une fois la taille maximale dépassée, le démon Docker tue le conteneur

Lorsque l'exécution d'un conteneur est interrompue de manière inopportune, il est possible d'utiliser la commande inspect de Docker sur le conteur pour obtenir les raisons de cet arrêt.

Une solution possible est de fixer la taille maximale du heap de la JVM en utilisant le paramètre -Xmx, mais cela implique qu'il faut configurer la mémoire deux fois, une fois pour le conteneur et une fois pour la JVM. De plus en cas de changement, il faut synchroniser l'autre valeur en correspondance.

Il est préférable de spécifier la taille du heap lors de l'utilisation de la JVM pour ne pas dépendre des valeurs déterminées par les ergonomics de la JVM.

De la même manière, la limitation de la CPU utilisable sur le conteneur peut avoir différents effets de bord. La JVM utilise le nombre de coeurs du système dans ses ergonomics. Le nombre de coeurs est obtenu du système hôte même si la JVM est exécutée dans un conteneur.

Si la JVM n'est pas capable d'obtenir les limitations en CPU dans cgroups, il faut préciser le nombre de threads utilisable par le ramasse-miette avec l'option -XX:ParallelGCThreads si le conteneur peut utiliser au moins 2 CPU. Si le conteneur ne peut utiliser qu'un seul CPU, il faut demander explicitement le ramasse-miettes Serial grâce à l'option -XX:+UseSerialGC pour éviter aux ergonomics de choisir un ramasse-miettes parallèle car l'hôte possède au moins 2 CPU.

 

66.3.2. Les évolutions dans Java SE 9 et Java SE 8u131

Java 8 update 131 (publié en avril 2017) inclut quelques améliorations pour faciliter la gestion de la mémoire et de la CPU pour une JVM exécutée dans un conteneur :

Java 9 propose plusieurs fonctionnalités expérimentales pour améliorer le support de Docker par la JVM. Ces fonctionnalités sont reportées de manière rétroactive dans Java 8u131 :

Comme elles sont expérimentales, il faut utiliser l'option -XX:+UnlockExperimentalVMOptions pour pouvoir les utiliser.

Java SE 9 et 8u131 propose une option expérimentale de la JVM pour récupérer la quantité de mémoire allouée au conteneur notamment lorsque celle-ci est limitée via cgroups. L'option -XX:+UseCGroupMemoryLimitForHeap demande à la JVM d'obtenir la taille maximale à partir de cgroups plutôt que du système hôte.

Si l'option -Xmx n'est pas utilisée, alors la JVM va rechercher la quantité de mémoire définie dans cgroups si des restrictions sont définies dans le conteneur.

Sans utiliser cette fonctionnalité, la JVM récupère toujours la taille de la RAM de la machine hôte (2Go pour la VM dans l'exemple ci-dessous) et ne tient pas compte des limitations configurées pour le conteneur.

Exemple :
$ docker run openjdk:8u131-jre-alpine java -XX:+PrintFlagsFinal -version | grep -E "(
InitialHeapSize|MaxHeapSize)"
    uintx InitialHeapSize             := 33554432        {product}
    uintx MaxHeapSize                 := 524288000       {product}
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (IcedTea 3.4.0) (Alpine 8.131.11-r2)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
 
$ docker run -m 1g openjdk:8u131-jre-alpine java -XX:+PrintFlagsFinal -version | grep -E "(
InitialHeapSize|MaxHeapSize)"
    uintx InitialHeapSize             := 33554432        {product}
    uintx MaxHeapSize                 := 524288000       {product}

Pour activer cette fonctionnalité, il faut utiliser deux options de la JVM :

-XX:+UnlockExperimentalVMOptions et -XX:+UseCGroupMemoryLimitForHeap

Exemple :
$ docker run -m 1g openjdk:8u131-jre-alpine java -XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap -XX:+PrintFlagsFinal -version | grep -E "(
InitialHeapSize|MaxHeapSize)"
    uintx InitialHeapSize             := 16777216        {product}
    uintx MaxHeapSize                 := 268435456       {product}

Au lieu d'utiliser la valeur de l'option -XX:MaxRAM, l'option -XX:+UseCGroupMemoryLimitForHeap lit et utilise la valeur du fichier /sys/fs/cgroup/memory/memory.limit_in_bytes :

Exemple :
$ docker run --rm -m 512m openjdk:9.0.4-jre cat /sys/fs/cgroup/memory/memory.limit_in_bytes
536870912
 
$ docker run --rm -m 512m openjdk:9.0.4-jre sh -c "java -XX:+PrintFlagsFinal -version | 
grep -Ei 'maxheapsize|maxram'"
openjdk version "9.0.4"
OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)
OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)
   size_t MaxHeapSize           = 524288000      {product} {ergonomic}
 uint64_t MaxRAM                = 137438953472   {pd product} {default}
    uintx MaxRAMFraction        = 4              {product} {default}
 
$ docker run --rm -m 512m openjdk:9.0.4-jre sh -c "java -XX:+PrintFlagsFinal
 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -version |
 grep -Ei 'maxheapsize|maxram'"
openjdk version "9.0.4"
OpenJDK Runtime Environment (build 9.0.4+12-Debian-4)
OpenJDK 64-Bit Server VM (build 9.0.4+12-Debian-4, mixed mode)
   size_t MaxHeapSize        = 134217728         {product} {ergonomic}
 uint64_t MaxRAM             = 137438953472      {pd product} {default}
    uintx MaxRAMFraction     = 4                 {product} {default}

Dans l'exemple ci-dessus, sans activer l'option, la taille maximale du heap est définie à 512Mo ce qui correspond à ¼ des 2Go de RAM de la machine hôte. Avec l'option activée, la taille maximale du heap est définie à 128Mo ce qui correspond à ¼ de la limitation à 512Mo de RAM pour le conteneur.

Il est aussi possible d'utiliser l'option -XX:MaxRAMFraction pour indiquer à la JVM quelle fraction de la mémoire maximale peut être utilisable par la JVM. Cependant la valeur est exprimée par une fraction de la taille totale.

L'option MaxRAMFraction permet d'exprimer la quantité de RAM utilisable par la JVM sous la forme d'une fraction. La valeur à fournir doit donc être un nombre entier strictement supérieur à zéro. La fraction (1/MaxRAMFraction) ainsi définie correspond à une proportion de MaxRAM utilisable pour le heap :

Cette option de la JVM pour contrôler la taille maximale du heap utilise une fraction plutôt qu'un pourcentage, ce qui rend difficile la définition de valeurs qui permettraient d'utiliser efficacement la RAM disponible.

Exemples avec un conteneur dont la mémoire est limitée à 1Go :

Par exemple, pour une JVM configurée avec MaxRAM à 1Go et MaxRAMFraction à 2, les ergonomics définissent la taille maximale du heap à 524Mo.

Exemple :
java -XX:+PrintFlagsFinal -XX:MaxRAM=1g -XX:MaxRAMFraction=2 -version | 
grep -Ei "maxheapsize|maxram"
   size_t MaxHeapSize          = 536870912    {product} {ergonomic}
 uint64_t MaxRAM               = 1073741824   {pd product} {command line}
    uintx MaxRAMFraction       = 2            {product} {command line}
java version "9.0.1"
Java(TM) SE Runtime Environment (build 9.0.1+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)

Il n'est pas recommandé d'utiliser la valeur 1 pour l'option -XX:MaxRAMFraction ce qui demande à la JVM d'utiliser l'intégralité de la RAM pour le heap. Or la JVM a besoin de mémoire en dehors du heap pour fonctionner. Le conteneur peut aussi exécuter des processus comme un shell qui ont aussi besoin de mémoire.

Il est préférable d'avoir un OutOfMemoryError de la JVM, notamment pour permettre de faire un heap dump et de l'analyser que de laisser le démon Docker tuer le conteneur avec un OOMKilled.

Le fait de ne pas utiliser -XX:MaxRAMFraction=1 implique de n'avoir au mieux que la moitié au maximum de mémoire heap utilisable par la JVM.

Avec Java 9, la JVM d'OpenJDK est capable de déterminer l'utilisation de cpusets pour la limitation CPU du conteneur et d'exploiter cette valeur. Attention avec Java 9, la JVM n'est pas en mesure de détecter une limitation avec cpu_shares.

Si les valeurs déterminées ne sont pas celles souhaitées, il est toujours possible de définir explicitement certains paramètres de la JVM notamment en utilisant les options -Xmx, - XX:ParallelGCThreads, -XX:ConcGCThreads, . en adéquation avec les limites fixées au conteneur.

 

66.3.3. Les évolutions dans Java SE 10 et Java SE 8u191

Java 10 propose un meilleur support de Docker par la JVM : plusieurs améliorations permettent à la JVM de tenir compte des restrictions de ressources appliquées sur le conteneur dans lequel elle s'exécute. C'est la première version de Java avec une prise en compte réelle automatique de l'exécution d'une JVM dans un conteneur Docker.

La prise en compte de la limitation des ressources CPU et de mémoire à un conteneur dans lequel une JVM est exécutée est grandement facilitée : la JVM détecte correctement la configuration du conteneur pour l'utiliser dans ses ergonomics.

Sur un système hôte Linux, la JVM est par défaut capable de détecter qu'elle s'exécute dans un conteneur Docker. Si c'est le cas, elle est en mesure d'obtenir des informations sur les ressources utilisables (CPU et mémoire) par le conteneur notamment si des restrictions lui sont appliquées plutôt que du système hôte.

Ceci est possible grâce à la nouvelle option -XX:+UseContainerSupport, activée par défaut pour une JVM exécutée sous Linux.

Exemple :
$ docker run -m 1g openjdk:10.0.2-jre java -XX:+PrintFlagsFinal -version |
grep -E "(UseContainerSupport)"
     bool UseContainerSupport            = true         {product} {default}
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)
OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)

L'option UseCGroupMemoryLimitForHeap est dépréciée car l'option UseContainerSupport la remplace.

L'utilisation de l'option UseCGroupMemoryLimitForHeap affiche un avertissement : «warning: Option UseCGroupMemoryLimitForHeap was deprecated in version 10.0 and will likely be removed in a future release".

Exemple :
$ docker run -m 1g openjdk:10.0.2-jre java -XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap -XX:+PrintFlagsFinal -version | 
grep -E "(InitialHeapSize|MaxHeapSize)"
OpenJDK 64-Bit Server VM warning: Option UseCGroupMemoryLimitForHeap was deprecated
in version 10.0 and will likely be removed in a future release.
   size_t InitialHeapSize           = 16777216       {product} {ergonomic}
   size_t MaxHeapSize               = 268435456      {product} {ergonomic}
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)
OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)
 
$ docker run -m 1g openjdk:10.0.2-jre java -XX:+PrintFlagsFinal -version |
grep -E "(InitialHeapSize|MaxHeapSize)"
   size_t InitialHeapSize           = 16777216       {product} {ergonomic}
   size_t MaxHeapSize               = 268435456      {product} {ergonomic}

Ce meilleur support par Java 10 facilite la configuration de la JVM via la ligne de commande ou le Dockerfile qui a moins besoin d'options dédiées pour une meilleure configuration par défaut.

La propriété UserContainerSupport est activée par défaut sous Linux. Il est possible de la désactiver en utilisant l'option -XX:-UseContainerSupport.

Trois Nouvelles options permettent d'améliorer le contrôle de la quantité de mémoire utilisable sous des valeurs exprimées en pourcentage :

Les options xxxRAMPercentage attendent une valeur comprise entre 0 et 100 qui permettent un contrôle très précis sur la quantité de mémoire utilisable pour le heap de la JVM

Par défaut avec un conteneur limité à 1Go, la taille minimale du heap est à 16Mo et la taille maximale à 256Mo.

Exemple :
$ docker run -m 1g openjdk:10.0.2-jre java -XX:+PrintFlagsFinal -version |
grep -E "(InitialHeapSize|MaxHeapSize)"
   size_t InitialHeapSize        = 16777216        {product} {ergonomic}
   size_t MaxHeapSize            = 268435456       {product} {ergonomic}
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)
OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)

Avec les options -XX:InitialRAMPercentage=10 et -XX:MaxRAMPercentage=80, la taille minimale du heap est à 104Mo et la taille maximale à 820Mo.

Exemple :
$ docker run -m 1g openjdk:10.0.2-jre java -XX:InitialRAMPercentage=10 
-XX:MaxRAMPercentage=80 -XX:+PrintFlagsFinal -version | grep -E "(InitialHeapSize|MaxHeapSize)"
   size_t InitialHeapSize        = 109051904     {product} {ergonomic}
   size_t MaxHeapSize            = 859832320     {product} {ergonomic}
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment (build 10.0.2+13-Debian-2)
OpenJDK 64-Bit Server VM (build 10.0.2+13-Debian-2, mixed mode)

Comme la JVM de Java 10 est capable de détecter son exécution dans un conteneur Docker, la prise en compte de la limitation de ressources ne requière plus de configuration spécifique.

Exemple :
$ docker run -it --entrypoint bash openjdk:10.0.2-jdk
root@1e39f4cf2236:/# jshell
Jul 20, 2019 8:01:59 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> Runtime runtime = Runtime.getRuntime();
runtime ==> java.lang.Runtime@27efef64
 
jshell> runtime.availableProcessors();
$2 ==> 2

La limitation de la CPU utilisable via le CPU set est correctement prise en compte dans la JVM.

Exemple :
$ docker run -it --cpuset-cpus 0 --entrypoint bash openjdk:10.0.2-jdk
root@32d18e9680dd:/# jshell
Jul 20, 2019 8:07:58 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 1

La limitation de la CPU utilisable via le CPU share est aussi correctement pris en compte dans la JVM.

Exemple :
$ docker run -it -c=512 --entrypoint bash openjdk:10.0.2-jdk
root@40ef1e702e0e:/# jshell
Jul 20, 2019 8:12:44 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro
 
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 1

Les fonctionnalités introduites dans Java 10 pour le support de l'exécution de la JVM dans des conteneurs Docker sont reportées dans l'update 191 de Java 8 :

 

66.3.4. Les évolutions dans Java SE 11

Une nouvelle option est ajoutée dans la JVM : -XX:+PreferContainerQuotaForCPUCount. Elle permet de demander la prise en compte de cpu_quota au lieu de cpu_shares pour déterminer le nombre de coeurs.

 

66.3.5. L'influence des ergonomics

Les ergonomics ont une influence sur les valeurs par défaut dans la JVM. Sur une machine hôte récente avec plusieurs CPU et Go de RAM, ces valeurs sont généralement largement suffisantes.

Lorsque l'on exécute plusieurs ou de nombreux conteneurs sur un même hôte, par exemple via un mécanisme d'orchestration, il est courant de limiter les ressources des conteneurs. Attention, ces limitations peuvent avoir des répercutions notamment sur les valeurs déterminées par les ergonomics.

Les exemples de cette section sont exécutés dans une VM disposant de 2 CPU et 2Go de RAM.

Le ramasse-miettes utilisé par défaut dépend du nombre de coeurs et de la RAM disponible.

Si le nombre de coeurs est supérieure ou égal à 2 alors le ramasse-miettes G1 est utilisé par la JVM Hotpost d'un JDK 11.

Exemple :
$ docker run --cpus="2" adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version |
grep -E "(UseSerialGC|UseG1GC|MaxHeapSize)"
   size_t MaxHeapSize             = 524288000         {product} {ergonomic}
     bool UseG1GC                 = true              {product} {ergonomic}
     bool UseSerialGC             = false             {product} {default}
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.3+7)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.3+7, mixed mode)

Si le nombre de coeurs est inférieure à 2 alors le ramasse-miettes SerialGC est utilisé par la JVM Hotpost d'un JDK 11 puisque le nombre de coeurs est insuffisant pour envisager des traitements en parallèle des activités du ramasse-miettes.

Exemple :
$ docker run --cpus="1" adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version |
grep -E "(UseSerialGC|UseG1GC|MaxHeapSize)"
   size_t MaxHeapSize              = 524288000        {product} {ergonomic}
     bool UseG1GC                  = false            {product} {default}
     bool UseSerialGC              = true             {product} {ergonomic}
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.3+7)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.3+7, mixed mode)

Selon la taille de la RAM (+/- 2Go) alors G1 ou SerialGC est utilisé par la JVM Hotpost d'un JDK 11.

Exemple :
$ docker run -m 1.8g adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version |
grep -E "(UseSerialGC|UseG1G
C|MaxHeapSize)"
   size_t MaxHeapSize              = 484442112        {product} {ergonomic}
     bool UseG1GC                  = true             {product} {default}
     bool UseSerialGC              = false            {product} {ergonomic}
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.3+7)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.3+7, mixed mode)
 
$ docker run -m 1.7g adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version |
grep -E "(UseSerialGC|UseG1G
C|MaxHeapSize)"
   size_t MaxHeapSize              = 457179136        {product} {ergonomic}
     bool UseG1GC                  = false            {product} {default}
     bool UseSerialGC              = true             {product} {ergonomic}
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.3+7)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.3+7, mixed mode)

La taille max par défaut du heap dépend de la taille totale de la RAM. A partir de 2Go de RAM, la taille max du heap est fixé à ¼ de la taille de la RAM. En dessous, la JVM utilise un ratio différent qui dépend de la taille de la RAM.

Exemple :
$ docker run -m 2g adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version | 
grep -E "MaxHeapSize"
   size_t MaxHeapSize            = 536870912        {product} {ergonomic} 
 
$ docker run -m 1g adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version | 
grep -E "MaxHeapSize"
   size_t MaxHeapSize            = 268435456        {product} {ergonomic}
 
$ docker run -m 512m adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version | 
grep -E "MaxHeapSize"
   size_t MaxHeapSize            = 134217728        {product} {ergonomic}
 
$ docker run -m 256m adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version | 
grep -E "MaxHeapSize"
   size_t MaxHeapSize            = 132120576        {product} {ergonomic}
 
$ docker run -m 128m adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version | 
grep -E "MaxHeapSize"
   size_t MaxHeapSize            = 67108864         {product} {ergonomic}
 
$ docker run -m 64m adoptopenjdk:11.0.3_7-jre-hotspot java -XX:+PrintFlagsFinal -version | 
grep -E "MaxHeapSize"
   size_t MaxHeapSize            = 33554432         {product} {ergonomic}

 

66.4. Le support de cgroups v2

A partir de sa version 20.10 de l'engine, Docker utilise cgroups (control groups) v2 à la place de cgroups v1.

Java 15 propose un support de cgroups v2 (JDK-8230305), les versions précédentes de Java utilisaient cgroups v1. Un backport a été effectué dans les versions 11.0.16 et 8u372 de Java.

Si la version de Java utilisée ne supporte que cgroups v1 et que le moteur d'exécution des conteneurs utilise cgroups v2, alors la détermination de la mémoire de l'environnement d'exécution par la JVM sera erronée ce qui pourra induire de OOM Kill par l'orchestrateur.

Il est possible de forcer (temporairement) le moteur Docker à utiliser cgroups v1 en utilisant l'option "deprecatedCgroupv1": true dans le fichier de configuration.


65. La gestion de la mémoire dans la JVM HotSpot 67. La décompilation et l'obfuscation Imprimer Index Index avec sommaire Télécharger le PDF    
Développons en Java
v 2.40   Copyright (C) 1999-2023 .