Un guide pour l’interprétation et la compilation JVM


  • FrançaisFrançais


  • Java est un langage indépendant de la plate-forme. Les programmes sont convertis en bytecode après compilation. Ce bytecode est converti en langage machine lors de l’exécution. Un interpréteur émule l’exécution d’instructions de bytecode pour la machine abstraite sur une machine physique spécifique. La compilation juste-à-temps (JIT) se produit à un moment donné pendant l’exécution, et la compilation en avance (AOT) se produit pendant le temps de construction.

    Cet article explique quand un interprète entre en jeu et quand JIT et AOT se produiront. Je discute également des compromis entre JIT et AOT.

    Code source, bytecode, code machine

    Les applications sont généralement écrites à l’aide d’un langage de programmation tel que C, C++ ou Java. L’ensemble d’instructions écrites à l’aide de langages de programmation de haut niveau est appelé code source. Le code source est lisible par l’homme. Pour l’exécuter sur la machine cible, le code source doit être converti en code machine, qui est lisible par machine. Le code source est généralement converti en code machine par un compilateur.

    En Java, cependant, le code source est d’abord converti en une forme intermédiaire appelée bytecode. Ce bytecode est indépendant de la plate-forme, c’est pourquoi Java est bien connu comme un langage de programmation indépendant de la plate-forme. Le compilateur Java principal javac convertit le code source Java en bytecode. Ensuite, le bytecode est interprété par l’interpréteur.

    Voici un petit Hello.java programme:

    //Hello.java
    public class Hello {

        public static void main(String[] args) {
             System.out.println("Inside Hello World!");
             }
    }

    Compilez-le en utilisant javac pour générer un Hello.class fichier contenant le bytecode.

    $ javac Hello.java
    $ ls
    Hello.class  Hello.java

    Maintenant, utilisez javap démonter le contenu de la Hello.class dossier. La sortie de javap dépend des options utilisées. Si vous ne choisissez aucune option, il imprime des informations de base, notamment le fichier source à partir duquel ce fichier de classe est compilé, le nom du package, les champs publics et protégés et les méthodes de la classe.

    $ javap Hello.class
    Compiled from "Hello.java"
    public class Hello {
      public Hello();
      public static void main(java.lang.String[]);
    }

    Pour voir le contenu du bytecode dans le .class fichier, utilisez le -c option:

    $ javap -c Hello.class
    Compiled from "Hello.java"
    public class Hello {
      public Hello();
            Code:
               0: aload_0
               1: invokespecial #1                      // Method java/lang/Object."<init>":()V
               4: return

      public static void main(java.lang.String[]);
            Code:
               0: getstatic         #2                      // Field java/lang/System.out:Ljava/io/PrintStream;
               3: ldc               #3                      // String Inside Hello World!
               5: invokevirtual #4                      // Method    
    java/io/PrintStream.println:(Ljava/lang/String;)V
               8: return
    }

    Pour obtenir des informations plus détaillées, utilisez le -v option:

    $ javap -v Hello.class

    Interprète, JIT, AOT

    L’interpréteur est chargé d’émuler l’exécution des instructions de bytecode pour la machine abstraite sur une machine physique spécifique. Lors de la compilation du code source à l’aide de javac et l’exécution à l’aide de java commande, l’interpréteur fonctionne pendant l’exécution et sert son objectif.

    $ javac Hello.java
    $ java Hello
    Inside Hello World!

    Le compilateur JIT fonctionne également au moment de l’exécution. Lorsque l’interpréteur interprète un programme Java, un autre composant, appelé un profileur d’exécution, surveille silencieusement l’exécution du programme pour observer quelle partie du code est interprétée et combien de fois. Ces statistiques permettent de détecter points chauds du programme, c’est-à-dire les portions de code fréquemment interprétées. Une fois qu’ils sont interprétés au-dessus d’un seuil défini, ils peuvent être convertis en code machine directement par le compilateur JIT. Le compilateur JIT est également appelé compilateur guidé par profil. La conversion du bytecode en code natif se produit à la volée, d’où le nom juste-à-temps. JIT réduit la surcharge de l’interpréteur en émulant le même ensemble d’instructions au code machine.

    Le compilateur AOT compile le code pendant la construction. La génération de code fréquemment interprété et compilé JIT au moment de la génération améliore le temps de préchauffage de la machine virtuelle Java (JVM). Ce compilateur a été introduit dans Java 9 en tant que fonctionnalité expérimentale. La jaotc L’outil utilise le compilateur Graal, lui-même écrit en Java, pour la compilation AOT.

    Voici un exemple de cas d’utilisation pour un programme Hello :

    //Hello.java
    public class Hello {

        public static void main(String[] args) {
                System.out.println("Inside Hello World!");
                }
    }

    $ javac Hello.java
    $ jaotc --output libHello.so Hello.class
    $ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libHello.so Hello
    Inside Hello World!

    Quand l’interprétation et la compilation entrent en jeu : un exemple

    Cet exemple illustre quand Java utilise un interpréteur et quand JIT et AOT interviennent. Considérez un programme Java simple, Demo.java:

    //Demo.java
    public class Demo {
      public int square(int i) throws Exception {
            return(i*i);
      }

      public static void main(String[] args) throws Exception {
            for (int i = 1; i <= 10; i++) {
              System.out.println("call " + Integer.valueOf(i));
              long a = System.nanoTime();
              Int r = new Demo().square(i);
            System.out.println("Square(i) = " + r);
              long b = System.nanoTime();
              System.out.println("elapsed= " + (b-a));
              System.out.println("--------------------------------");
            }
      }
    }

    Ce programme simple a un main méthode qui crée un Demo instance d’objet et appelle la méthode squarequi affiche la racine carrée de for valeur d’itération de la boucle. Maintenant, compilez et exécutez le code :

    $ javac Demo.java
    $ java Demo
    1 iteration
    Square(i) = 1
    Time taken= 8432439
    --------------------------------
    2 iteration
    Square(i) = 4
    Time taken= 54631
    --------------------------------
    .
    .
    .
    --------------------------------
    10 iteration
    Square(i) = 100
    Time taken= 66498
    --------------------------------

    La question est maintenant de savoir si la sortie est le résultat de l’interpréteur, JIT ou AOT. Dans ce cas, il est entièrement interprété. Comment ai-je conclu cela? Eh bien, pour que JIT contribue à la compilation, les points chauds du code doivent être interprétés au-dessus d’un seuil défini. Alors et seulement alors ces morceaux de code sont mis en file d’attente pour la compilation JIT. Pour trouver le seuil pour JDK 11 :

    $ java -XX:+PrintFlagsFinal -version | grep CompileThreshold
     intx CompileThreshold     = 10000                                      {pd product} {default}
    [...]
    openjdk version "11.0.13" 2021-10-19
    OpenJDK Runtime Environment 18.9 (build 11.0.13+8)
    OpenJDK 64-Bit Server VM 18.9 (build 11.0.13+8, mixed mode, sharing)

    La sortie ci-dessus montre qu’un morceau de code particulier doit être interprété 10 000 fois pour être éligible à la compilation JIT. Ce seuil peut-il être réglé manuellement et existe-t-il un indicateur JVM indiquant si une méthode est compilée JIT ? Oui, il existe plusieurs options pour atteindre cet objectif.

    Une option pour savoir si une méthode est compilée JIT est -XX:+PrintCompilation. Parallèlement à cette option, le drapeau -Xbatch fournit la sortie d’une manière plus lisible. Si l’interprétation et le JAT se déroulent en parallèle, le -Xbatch flag aide à distinguer la sortie des deux. Utilisez ces drapeaux comme suit :

    $ java -Xbatch  -XX:+PrintCompilation  Demo
             34        1        b  3           java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
             35        2         n 0           jdk.internal.misc.Unsafe::getObjectVolatile (native)   
             35        3        b  3           java.lang.Object::<init> (1 bytes)
    [...]
    210  269         n 0           java.lang.reflect.Array::newArray (native)   (static)
            211  270        b  3           java.lang.String::substring (58 bytes)
    [...]
    --------------------------------
    10 iteration
    Square(i) = 100
    Time taken= 50150
    -------------------------------- 

    La sortie de la commande ci-dessus est trop longue, j’ai donc tronqué la partie centrale. Notez qu’avec le code du programme de démonstration, les fonctions de classe internes du JDK sont également compilées. C’est pourquoi la sortie est si longue. Parce que mon objectif est Demo.java code, j’utiliserai une option qui peut minimiser la sortie en excluant les fonctions internes du package. La commande –XX:CompileCommandFile désactive JIT pour les classes internes :

    $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Demo

    Le fichier hotspot_compiler référencé par -XX:CompileCommandFile contient ce code pour exclure des packages spécifiques :

    $ cat hotspot_compiler
    quiet
    exclude java/* *
    exclude jdk/* *
    exclude sun/* *

    En première ligne, quiet indique à la JVM de ne rien écrire sur les classes exclues. Pour régler le seuil JIT, utilisez -XX:CompileThreshold avec la valeur définie sur 5, ce qui signifie qu’après avoir interprété cinq fois, il est temps pour JIT :

    $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
    -XX:CompileThreshold=5 Demo
            47      1       n 0     java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)  
               (static)
            47      2       n 0     java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)  
            47      3       n 0     java.lang.invoke.MethodHandle::linkToSpecial(LLLLLLL)L (native)  
               (static)
            48      4       n 0     java.lang.invoke.MethodHandle::linkToStatic(L)I (native)   (static)
            48      5       n 0     java.lang.invoke.MethodHandle::invokeBasic()I (native)  
            48      6       n 0     java.lang.invoke.MethodHandle::linkToSpecial(LL)I (native)  
               (static)
    [...]
            1 iteration
            69   40         n 0     java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native)  
               (static)
    [...]
    Square(i) = 1
            78   48         n 0     java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native)  
    (static)
            79   49         n 0     java.lang.invoke.MethodHandle::invokeBasic(ILIJ)I (native)  
    [...]
            86   54         n 0     java.lang.invoke.MethodHandle::invokeBasic(J)L (native)  
            87   55         n 0     java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native)  
    (static)
    Time taken= 8962738
    --------------------------------
    2 iteration
    Square(i) = 4
    Time taken= 26759
    --------------------------------

    10 iteration
    Square(i) = 100
    Time taken= 26492
    --------------------------------

    La sortie n’est toujours pas différente de la sortie interprétée ! C’est parce que, selon la documentation d’Oracle, le -XX:CompileThreshold flag n’est effectif que lorsque TieredCompilation est désactivé :

    $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
    -XX:-TieredCompilation -XX:CompileThreshold=5 Demo
    124     1       n       java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)   (static)
    127     2       n       java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)  
    [...]
    1 iteration
            187   40        n       java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native)   (static)
    [...]
    (native)   (static)
            212   54        n       java.lang.invoke.MethodHandle::invokeBasic(J)L (native)  
            212   55        n       java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native)   (static)
    Time taken= 12337415
    [...]
    --------------------------------
    4 iteration
    Square(i) = 16
    Time taken= 37183
    --------------------------------
    5 iteration
            214   56        b       Demo::<init> (5 bytes)
            215   57        b       Demo::square (16 bytes)
    Square(i) = 25
    Time taken= 983002
    --------------------------------
    6 iteration
    Square(i) = 36
    Time taken= 81589
    [...]
    10 iteration
    Square(i) = 100
    Time taken= 52393

    Cette section de code est maintenant compilée JIT après la cinquième interprétation :

    --------------------------------
    5 iteration
            214   56        b       Demo::<init> (5 bytes)
            215   57        b       Demo::square (16 bytes)
    Square(i) = 25
    Time taken= 983002
    --------------------------------

    Avec le square() méthode, le constructeur obtient également JIT compilé car il y a une instance Demo à l’intérieur de la for boucle avant d’appeler square(). Par conséquent, il atteindra également le seuil et sera compilé JIT. Cet exemple illustre le moment où le JIT entre en jeu après l’interprétation.

    Pour voir la version compilée du code, utilisez le -XX:+PrintAssembly flag, qui ne fonctionne que s’il y a un désassembleur dans le chemin de la bibliothèque. Pour OpenJDK, utilisez le hsdis désassembleur. Téléchargez une bibliothèque de désassembleur appropriée – dans ce cas, hsdis-amd64.so— et placez-le sous Java_HOME/lib/server. Assurez-vous d’utiliser -XX:+UnlockDiagnosticVMOptions avant de -XX:+PrintAssembly. Sinon, JVM vous donnera un avertissement.

    La commande entière est la suivante :

    $ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \ -XX:-TieredCompilation -XX:CompileThreshold=5 -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly Demo
    [...]
    5 iteration
            178   56        b       Demo::<init> (5 bytes)
    Compiled method (c2)    178   56                Demo::<init> (5 bytes)
     total in heap  [0x00007fd4d08dad10,0x00007fd4d08dafe0] = 720
     relocation     [0x00007fd4d08dae88,0x00007fd4d08daea0] = 24
    [...]
     handler table  [0x00007fd4d08dafc8,0x00007fd4d08dafe0] = 24
    [...]
     dependencies   [0x00007fd4d08db3c0,0x00007fd4d08db3c8] = 8
     handler table  [0x00007fd4d08db3c8,0x00007fd4d08db3f8] = 48
    ----------------------------------------------------------------------
    Demo.square(I)I  [0x00007fd4d08db1c0, 0x00007fd4d08db2b8]  248 bytes
    [Entry Point]
    [Constants]
      # {method} {0x00007fd4b841f4b0} 'square' '(I)I' in 'Demo'
      # this:       rsi:rsi   = 'Demo'
      # parm0:      rdx     = int
      #             [sp+0x20]  (sp of caller)
    [...]
    [Stub Code]
      0x00007fd4d08db280: movabs $0x0,%rbx          ;   {no_reloc}
      0x00007fd4d08db28a: jmpq   0x00007fd4d08db28a  ;   {runtime_call}
      0x00007fd4d08db28f: movabs $0x0,%rbx          ;   {static_stub}
      0x00007fd4d08db299: jmpq   0x00007fd4d08db299  ;   {runtime_call}
    [Exception Handler]
      0x00007fd4d08db29e: jmpq   0x00007fd4d08bb880  ;   {runtime_call ExceptionBlob}
    [Deopt Handler Code]
      0x00007fd4d08db2a3: callq  0x00007fd4d08db2a8
      0x00007fd4d08db2a8: subq   $0x5,(%rsp)
      0x00007fd4d08db2ad: jmpq   0x00007fd4d08a01a0  ;   {runtime_call DeoptimizationBlob}
      0x00007fd4d08db2b2: hlt    
      0x00007fd4d08db2b3: hlt    
      0x00007fd4d08db2b4: hlt    
      0x00007fd4d08db2b5: hlt    
      0x00007fd4d08db2b6: hlt    
      0x00007fd4d08db2b7: hlt    
    ImmutableOopMap{rbp=NarrowOop }pc offsets: 96
    ImmutableOopMap{}pc offsets: 112
    ImmutableOopMap{rbp=Oop }pc offsets: 148 Square(i) = 25
    Time taken= 2567698
    --------------------------------
    6 iteration
    Square(i) = 36
    Time taken= 76752
    [...]
    --------------------------------
    10 iteration
    Square(i) = 100
    Time taken= 52888

    La sortie est longue, j’ai donc inclus uniquement la sortie liée à Demo.java.

    Il est maintenant temps pour la compilation AOT. Cette option a été introduite dans JDK9. AOT est un compilateur statique pour générer le .so bibliothèque. Avec AOT, les classes intéressées peuvent être compilées pour créer un .so bibliothèque qui peut être directement exécutée au lieu d’interpréter ou de compiler JIT. Si JVM ne trouve aucun code compilé par AOT, l’interprétation habituelle et la compilation JIT ont lieu.

    La commande utilisée pour la compilation AOT est la suivante :

    $ jaotc --output=libDemo.so Demo.class

    Pour voir les symboles dans la bibliothèque partagée, utilisez ce qui suit :

    $ nm libDemo.so

    Pour utiliser le produit généré .so bibliothèque, utiliser -XX:AOTLibrary de même que -XX:+UnlockExperimentalVMOptions comme suit:

    $ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so Demo
    1 iteration
    Square(i) = 1
    Time taken= 7831139
    --------------------------------
    2 iteration
    Square(i) = 4
    Time taken= 36619
    [...]
    10 iteration
    Square(i) = 100
    Time taken= 42085

    Cette sortie ressemble à une version interprétée elle-même. Pour vous assurer que le code compilé AOT est utilisé, utilisez -XX:+PrintAOT:

    $ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
             28        1         loaded        ./libDemo.so  aot library
             80        1         aot[ 1]   Demo.main([Ljava/lang/String;)V
             80        2         aot[ 1]   Demo.square(I)I
             80        3         aot[ 1]   Demo.<init>()V
    1 iteration
    Square(i) = 1
    Time taken= 7252921
    --------------------------------
    2 iteration
    Square(i) = 4
    Time taken= 57443
    [...]
    10 iteration
    Square(i) = 100
    Time taken= 53586

    Juste pour vous assurer que la compilation JIT ne s’est pas produite, utilisez ce qui suit :

    $ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation \ -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation \ -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
             19        1         loaded        ./libDemo.so  aot library
             77        1         aot[ 1]   Demo.square(I)I
             77        2         aot[ 1]   Demo.main([Ljava/lang/String;)V
             77        3         aot[ 1]   Demo.<init>()V
             77        2         aot[ 1]   Demo.main([Ljava/lang/String;)V   made not entrant
    [...]
    4 iteration
    Square(i) = 16
    Time taken= 43366
    [...]
    10 iteration
    Square(i) = 100
    Time taken= 59554

    Si une petite modification est apportée au code source soumis à AOT, il est important de s’assurer que le .so est à nouveau créé. Sinon, l’AOT compilé obsolète .so n’aura aucun effet. Par exemple, apportez une petite modification à la fonction square de sorte qu’elle calcule maintenant le cube :

    //Demo.java
    public class Demo {

      public int square(int i) throws Exception {
            return(i*i*i);
      }

      public static void main(String[] args) throws Exception {
            for (int i = 1; i <= 10; i++) {
              System.out.println("" + Integer.valueOf(i)+" iteration");
              long start = System.nanoTime();
              int r= new Demo().square(i);
              System.out.println("Square(i) = " + r);
              long end = System.nanoTime();
              System.out.println("Time taken= " + (end-start));
              System.out.println("--------------------------------");
            }
      }
    }

    Maintenant, compilez Demo.java encore:

    $ java Demo.java

    Mais ne créez pas libDemo.so utilisant jaotc. Utilisez plutôt cette commande :

    $ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
             20        1         loaded        ./libDemo.so  aot library
             74        1         n           java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)   (static)
    2 iteration
    sqrt(i) = 8
    Time taken= 43838
    --------------------------------
    3 iteration
            137   56        b            Demo::<init> (5 bytes)
            138   57        b            Demo::square (6 bytes)
    sqrt(i) = 27
    Time taken= 534649
    --------------------------------
    4 iteration
    sqrt(i) = 64
    Time taken= 51916
    [...]
    10 iteration
    sqrt(i) = 1000
    Time taken= 47132

    Bien que l’ancienne version de libDemo.so est chargé, JVM l’a détecté comme obsolète. Chaque fois qu’un .class est créé, une empreinte digitale va dans le fichier de classe et une empreinte digitale de classe est conservée dans la bibliothèque AOT. Étant donné que l’empreinte de classe est différente de celle de la bibliothèque AOT, le code natif compilé par AOT n’est pas utilisé. Au lieu de cela, la méthode est maintenant compilée JIT, car le -XX:CompileThreshold est fixé à 3.

    AOT ou JIT ?

    Si vous souhaitez réduire le temps de préchauffage de la JVM, utilisez AOT, ce qui réduit la charge pendant l’exécution. Le hic, c’est qu’AOT n’aura pas assez de données pour décider quel morceau de code doit être précompilé en code natif. En revanche, JIT intervient pendant l’exécution et a un impact sur le temps de préchauffage. Cependant, il disposera de suffisamment de données de profilage pour compiler et décompiler le code plus efficacement.

    Source

    La Rédaction

    L'équipe rédactionnnelle du site

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code