6
Comme mentionné dans la première partie de cette série d’articles sur la JVM, le bytecode généré par un compilateur statique , est chargé dans la JVM grâce aux chargeurs de classe. Ensuite, ledit code écrit en langage intermédiaire non compréhensible par le microprocesseur, est interprété ligne par ligne par

Architecture de la jvm deuxieme partie

Embed Size (px)

Citation preview

Page 1: Architecture de la jvm deuxieme partie

Comme mentionné dans la première partie de cette série d’articles sur la JVM, le bytecode généré par un compilateur statique , est chargé dans la JVM grâce aux chargeurs de classe. Ensuite, ledit code écrit en langage intermédiaire non compréhensible par le microprocesseur, est interprété ligne par ligne par l’interpréteur qui fait partie de l’architecture de la JVM, ledit code est transformé en code assembleur pour être reconnu par le microprocesseur, puis en

Page 2: Architecture de la jvm deuxieme partie

code machine, pour être enfin exécuté. Cependant, pendant l’exécution, la JVM compte le nombre de fois qu’un code est exécuté, si elle constate l’appel fréquent d’un code, celui-ci est éligible à la compilation par la JIT (Jus-In-Time Compiler) qui est un compilateur dynamique faisant aussi partie de la JVM. Dans cet article, nous allons parler des rôles de l’interpréteur et du compilateur JIT dans le processus d’exécution du code chargé dans la JVM.

I-L’interpréteurL’interpréteur implémenté dans Hotspot (JVM oracle) est basé sur un Template. L’implémentation d’un interpréteur peut se faire soit en mode Template comme c’est le cas de Hotspot ou en mode switch case dans une boucle, les deux modes ont des avantages et des inconvénients que nous n’allons pas détailler dans cet article.

Pendant le démarrage de la JVM, celle-ci génère en mémoire l’interpréteur en utilisant des informations stockées dans un template appelé TemplateTable. Cette structure de donnée contient le mapping entre chaque instruction de bytecode et l’instruction dédiée à chaque type de machine. La JVM fournit aussi des accesseurs sous forme de fonctions pour accéder au contenu du TemplateTable.

Après le chargement du bytecode, le code n’est pas compilé immédiatement, ceci pour deux raisons :

La première raison, est la fréquence d’exécution du code. En effet, si un code est appelé une fois il n’est donc pas nécessaire de le compiler, il serait beaucoup plus judicieux qu’une interprétation soit faite pour éviter une consommation de ressources inutiles, car compiler du code exécuté une seule fois nécessite plusieurs cycles processeur, cependant, le prix à payer, est la lenteur d’exécution. En d’autres mots le code interprété est moins rapide dans l’exécution que le code compilé.

La deuxième raison c’est l’optimisation. Un code fréquemment exécuté permet à la JVM de glaner des informations qu’elle utilisera pendant la compilation aux fins d’optimisation.

Afin d’illustrer nos propos, prenons une méthode fairequelqueChose() qui est définie dans une classe X, héritée et redéfinie par une autre classe Y, lors de l’appel de celle-ci sur une instance de Y (y1. fairequelqueChose()), la JVM va faire une recherche(lookup) sur l’appel de ladite méthode afin de déterminer la méthode de la classe à exécuter. Par le mécanisme de polymorphisme, fairequelqueChose() de Y va être exécutée. Ce processus (recherche – exécution (interprétation)) va se répéter tant aussi longtemps que le code n’est pas compilé. Après plusieurs exécutions, la JVM va pouvoir glaner des informations sur ladite méthode et se rendre compte que fairequelqueChose() est appelé par l’instance de Y. Ces informations seront ensuite utilisées pendant la phase de compilation. La JVM va optimiser son processus en éliminant l’étape de recherche, ce qui va permettre à la JVM de gagner en célérité dans son processus.

Page 3: Architecture de la jvm deuxieme partie

Figure 1

Il existe dans la JVM 5 niveaux de compilation qui représentent l’état de compilation d’un code dans la JVM :

Niveau 0 : Code interprété Niveau 1 : C1 (compilateur client) code compilé simplifié Niveau 2 : C1 (Compilateur client) code compilé limité Niveau 3 : C1 (Compilateur client) code compilé en entier Niveau 4 : C2 (Compilateur serveur) code compilé

Comme nous pouvons le constater, le code interprété est au niveau 0. Il représente le tout premier état d’un code chargé dans la JVM pendant l’exécution. Il est à noter que tout code

Page 4: Architecture de la jvm deuxieme partie

chargé dans la JVM commence au niveau 0 avant de poursuivre son chemin dans une file de compilation. Nous constatons également qu’il existe deux types de compilateur dans la JVM, le compilateur client C1 et le compilateur serveur C2.

II-CompilateurPendant l’exécution d’une méthode, la JVM établit 2 compteurs, 1 pour la fréquence d’appel de ladite méthode et un deuxième pour chaque branchement de la boucle, si la méthode contient une boucle. Ces deux compteurs sont donc comparés à une variable paramétrable : CompileThreshold. Cette variable par défaut dans le compilateur client est 1500 et pour le compilateur serveur est 10000. Elle peut faire partie des paramètres de calibrage de la JVM en indiquant : -XX :compileThreshold=N et N représente le nombre de fois nécessaire pour qu’un code soit appelé afin d’ être éligible à une compilation.

Le compilateur JIT existe en deux types, le client et le serveur. Dans la JVM, ils sont appelés respectivement compilateur C1 et compilateur C2. Pour l’utilisation, il faut mentionner la commande –client ou –server dans le calibrage de la JVM, en passant ces variables dans les arguments d’exécution de la JVM.

La différence entre les 2 types de compilateurs est la réactivité face au code à compiler, le compilateur client est beaucoup plus rapide dans la compilation au début de l’exécution que le compilateur serveur. En revanche, le compilateur C2 (serveur) va produire une meilleure optimisation que le compilateur C1. Ce qui implique un certain gain en termes de rapidité d’exécution du code compilé par C2.

Depuis la version 7 de java, il existe une autre forme de compilation, qui utilise les deux types de compilateurs comme levier. Appelée TieredCompilation, et paramétrée au lancement de la JVM avec cet argument –XX :+TieredCompilation , elle permet de profiter de la célérité de compilation du code compilé de C1 au démarrage de l’exécution et de la qualité d’optimisation du code compilé par C2.

Dans le processus de compilation, la JVM utilise une ressource appelée code cache qui lui permet d’optimiser son processus. La taille de cette structure a une incidence sur la quantité du code à compiler. Elle permet à la JVM d’y stocker certaines instructions en assembleur à utiliser pour la compilation. Ayant une taille par défaut, elle peut tout de même être calibrée en utilisant cette commande : -XX:ReservedCodeCacheSize=N , N étant la taille du code cache.

Lorsque la JVM constate qu’un code est éligible à la compilation, il le met dans une file d’attente, ensuite selon la priorité qui est établie par le nombre d’appels dudit code, il est donc acheminé vers le compilateur.

Pour visualiser les logs de compilations, il faut calibrer la JVM avec cette argument –XX :+PrintCompilation.