Published by et

Il y a 3 semaines -

Temps de lecture 11 minutes

GraalVM – Tour d’horizon

Introduction

GraalVM est une machine virtuelle universelle développée par Oracle. Sortie en 2018, elle n’en reste pas moins dans une phase de développement très active et n’a d’ailleurs connu sa première version LTS (Long-Term Support) que fin 2019.

Cette machine virtuelle comporte deux principales particularités :

  • elle est polyglotte et supporte ainsi plusieurs langages en leur permettant même de cohabiter dans un même environnement
  • elle permet de générer des images natives avec pour but de réduire considérablement le temps de démarrage des applications ainsi que leur empreinte mémoire

Nous allons vous présenter à travers cet article le fonctionnement global de GraalVM afin de mieux appréhender son utilisation. Un second article complétera celui-ci afin de vous faire entrer plus avant dans le code.

Le Graal Compiler, brique principale de GraalVM

Le Graal Compiler est le cœur de GraalVM. Il s’agit d’un nouveau compilateur JIT ayant pour objectif de remplacer le compilateur C2 de Hotspot.

Pour rappel, Hotspot est la machine virtuelle utilisée dans l’implémentation actuelle de Java dans le projet OpenJDK.
Hotspot contient deux compilateurs Just-in-Time, le C1 et le C2. Java est un langage interprété, ce qui est en général moins performant qu’un langage compilé nativement. C’est là qu’interviennent les compilateurs JIT. Leur rôle est d’analyser les instructions régulièrement exécutées pour les compiler nativement à la volée et les rendre ainsi plus performantes.
Le compilateur C1 de HotSpot entreprend une première phase d’optimisation de manière assez simple et rapide. Le compilateur C2 intervient ensuite pour optimiser le code de manière beaucoup plus fine. C2 est une brique très importante de la JVM puisqu’il permet de rendre le code exécuté parfois plus performant que du C++.

Le problème du compilateur C2 actuel est sa complexité :

  • Étant écrit en C++, des erreurs peuvent rapidement survenir et stopper la JVM
  • Le code est devenu globalement difficilement maintenable et évolutif

Avec Graal, Oracle a donc décidé de réécrire un nouveau compilateur JIT. La structure entière du compilateur a été repensée pour y ajouter de nouvelles optimisations afin d’améliorer les performances et surtout rendre le projet bien plus maintenable et évolutif.
C’est pourquoi ce dernier a été écrit en Java, apportant ainsi de nombreux avantages :

  • Gestion de la mémoire sécurisée, simplifiée et contrôlée
  • Écosystème Java particulièrement riche
  • Beaucoup plus compréhensible par les développeurs Java, ces derniers étant les plus impactés par ce compilateur.

Ce compilateur est indépendant de Hotspot mais pas incompatible. Ainsi, vous pouvez l’utiliser aussi bien avec GraalVM que sur votre JVM classique.

Depuis Java 10, vous pouvez utiliser les options de lancement suivantes pour pouvoir activer le compilateur Graal :

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

Vous avez dit polyglotte ?

Une des principales particularités de GraalVM est son universalité. Ainsi vous pouvez l’utiliser avec un grand nombre de langages tels que :

  • Les langages compatibles avec la JVM : Java, Scala et Kotlin
  • Les langages dynamiques tels que JavaScript, Ruby, Python
  • Les langages compilés nativement comme le C, C++, Rust

Cette universalité est rendue possible grâce au framework Truffle. Ce dernier permet, dans les grandes lignes, de créer des interpréteurs d’arbres syntaxiques.

Un arbre syntaxique est un arbre dont les nœuds représentent des opérateurs du langage, des expressions ou encore des variables. En lisant un tel arbre, il est possible de déterminer les instructions devant être exécutées.

Les langages dynamiques tels que JavaScript ou Ruby par exemple sont interprétés de cette manière.

L’intégration des interpréteurs Truffle dans GraalVM permet d’interpréter ces langages dans la JVM et même d’y associer les performances du JIT. On obtient, outre l’utilisation de plusieurs langages dans un même environnement, de très bonnes performances sur les langages ainsi interprétés.
Pour JavaScript par exemple, nous obtenons des performances équivalentes au V8 utilisé par Chrome.
Un des objectifs de Truffle est d’obtenir les meilleures performances possibles pour chaque langage.

Les langages natifs comme le C et le C++, utilisent un interpréteur particulier : Sulong. Ce dernier a été créé avec Truffle, mais au lieu d’interpréter directement un langage, il a pour but d’interpréter le byte code LLVM généré lors de la compilation des langages natifs. Ce projet est encore récent et a encore besoin d’être amélioré pour obtenir de bonnes performances et une compatibilité totale avec tous les langages natifs : C, C++, Rust, Objective C, Fortran…

Les images natives

Un des principaux objectifs de GraalVM, qui va d’ailleurs grandement nous intéresser dans le monde Java, est la possibilité de générer des images natives.

Reprenons notre Graal Compiler détaillé ci-dessus. Celui-ci possède un mode spécial permettant d’effectuer une compilation anticipée, ou ahead-of-time.
Contrairement à la compilation “classique” où notre code est transformé en byte code qui sera optimisé via le JIT, la compilation anticipée transforme tout de suite notre code en code natif, exécutable directement sur la machine cible. Cet exécutable est ce que l’on appelle une image native.
La génération de cette image est possible sur différents OS :

  • Linux
  • MacOS
  • Windows

L’objectif principal de l’image native est l’optimisation du code dès la compilation pour obtenir un gain en terme de temps de démarrage et d’utilisation mémoire.

Penchons nous sur le fonctionnement de cette compilation.

Optimisation par l’élimination de code mort

La première étape de la compilation anticipée passe par une compilation classique en byte code. Ce dernier sera ensuite analysé afin de déterminer l’entièreté des chemins qui seront empruntés lors de l’exécution du code et ainsi supprimer tout ce qui est inutile. Cela revient à supprimer de votre code toutes les classes, méthodes, attributs, variables qui ne sont jamais utilisés.
Cette optimisation de code nécessite d’être dans ce que l’on appelle une « closed world assumption », où le code entier est connu dès la compilation, donc qu’aucun nouveau code est créé lors de l’exécution. Nous verrons plus tard que cela peut rendre incompatible certaines fonctionnalités Java telle que la réflexion.

Pour finir, le byte code optimisé est compilé en un exécutable natif.

SubstrateVM

Dans une application Java classique, la JVM nous apporte de nombreuses fonctionnalités, dont certaines indispensables tel que le Garbage Collector. GraalVM ne les oublie pas et connaît sa propre manière de les incorporer.

Lors de la génération de l’image native, un composant va être associé à notre code : SubstrateVM.
C’est une sorte de nouvelle JVM qui offre des solutions pour tous les composants nécessaires lors de l’exécution du programme : Garbage Collector, gestionnaire de threads, deoptimizer…
Ce subset de JVM est compilé de la même manière que votre code et est ainsi directement incorporé dans votre exécutable natif.

Cette fois ci, notre image native est complète et fonctionnelle ! On peut tout de suite y voir plusieurs intérêts :

  • Temps de démarrage beaucoup plus court. En effet, une application Java classique nécessite un « temps de chauffe », dû entre autres au démarrage et au fonctionnement de la JVM et surtout à notre JIT qui doit laisser tourner l’application un certain temps avant de l’optimiser. Ici nous avons déjà un code optimisé et natif, qui démarre donc beaucoup plus rapidement.
    En général, une application compilée de cette manière met quelques dizaines de millisecondes pour démarrer, contre des temps de démarrage de plusieurs secondes pour une compilation classique.
  • Consommation mémoire plus faible. La JVM, du fait de ses nombreuses fonctionnalités, consomme une certaine quantité de mémoire (métadonnées, JIT, GC), parfois 10 fois supérieure à la quantité de mémoire utilisée par l’application elle même. L’image native ne comportant que quelques composants essentiels de la JVM et n’effectuant que peu de traitements pendant l’exécution (pas d’interprétation du byte code, pas de JIT, pas de statistiques), sa consommation mémoire est de fait plus réduite.
    À titre d’exemple, d’après nos propres tests, un simple Hello World consomme environ 21Mo de mémoire en temps normal contre seulement 3Mo avec l’image native. Nous constatons donc une diminution d’un facteur 7.

Limitations

Nous avons évoqué un peu plus tôt que certaines fonctionnalités de Java, notamment la réflexion, pourraient être impactées par la compilation anticipée de GraalVM.

En effet, ce type de fonctionnalités permet de modifier le code pendant l’exécution, entrainant des inconnues lors de la compilation et allant ainsi à l’encontre de la closed world assumption. GraalVM offre tout de même des solutions pour certaines d’entre elles :

  • La réflexion
  • Les proxies dynamiques
  • Le class loading dynamique

Nous pouvons les utiliser grâce à des fichiers de configuration. Ainsi, il suffit de remplir la liste des classes sur lesquelles on veut effectuer de la réflexion ou encore du class loading dynamique. Ces classes seront ainsi épargnées par l’élimination de code mort.

Cependant, d’autres fonctionnalités Java ou liées à la JVM n’ont aucune compatibilité avec GraalVM. On peut notamment citer :

  • les invokedynamic, qui introduisent des changements pendant l’exécution, ne sont par définition pas compatibles avec une compilation anticipée
  • le Security Manager
  • JVMTI, un outil de monitoring basé sur le byte code, devient incompatible avec notre code binaire
  • La sérialisation actuelle de Java, qui est basée sur des métadonnées inexistantes dans l’image native, est également à exclure et est en attente d’une nouvelle solution technique

Ces limitations, malgré la solution de configuration pour certaines d’entre elles, entrainent une grande difficulté à adapter du code existant. En effet, certaines fonctionnalités utilisées peuvent faire partie de la liste d’incompatibilité, ou nécessiter de la configuration parfois complexe.

N’oublions pas l’utilisation fréquente de frameworks tels que Spring ou Hibernate qui utilisent en masse la réflexion et sont donc totalement incompatibles avec les image natives de GraalVM. Les adapter demande un travail colossal devant être réalisé par les développeurs mêmes de ces frameworks.
C’est d’ailleurs le cas de Spring qui offre une première intégration de GraalVM au sein du framework.
Hibernate, quant à lui, a été adapté par le framework Quarkus afin d’être utilisable, en plus de bien d’autres bibliothèques et frameworks, dans GraalVM.

Et pour les applications polyglottes ?

Les images natives sont également disponibles pour les applications polyglottes. Il est uniquement nécessaire de spécifier les langages utilisés dans l’application.

Conclusion

Entre son approche polyglotte et ses images natives, GraalVM propose de nouvelles perspectives en terme de développement d’applications. Dans cet article, nous nous sommes essentiellement concentrés sur le monde Java et donc aux images natives.

Grâce à ces dernières, GraalVM offre un nouveau paradigme en terme de compilation de nos applications Java, allant à l’encontre de beaucoup d’approches actuelles, notamment les frameworks classiques tels que Spring et Hibernate qui effectuent de nombreuses initialisations pendant l’exécution grâce à la réflexion.

Cette approche apporte de gros bénéfices en terme de temps de démarrage et de consommation mémoire, mais s’accompagne de plusieurs inconvénients.

  • La compilation étant complexe, celle-ci peut durer beaucoup de temps, pouvant atteindre 5min pour un simple Hello World. Ce temps de compilation va cependant en s’améliorant au fur et à mesure des nouvelles versions.
  • De nombreuses fonctionnalités Java ou liées à la JVM sont incompatibles ou nécessitent de la configuration, rendant souvent impossible l’adaptabilité de nos applications, notamment au niveau des frameworks.

Nous voyons tout de même apparaître de nouveaux frameworks, notamment Quarkus et Micronaut, qui nous offrent tous les outils nécessaires pour créer des applications complètes compatibles avec GraalVM. Ces frameworks sont encore jeunes et demandant de la maturité, mais ils sont sur la bonne voie pour permettre à GraalVM de s’imposer comme un nouveau standard de Java.

Dans l’article suivant, nous vous présenterons de nombreux exemples de code afin de jouer avec le concept d’image native et ainsi entrer dans la pratique !

Published by et

Commentaire

Laisser un commentaire

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

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.