Graal VM, qu’est ce que c’est, quels usages ?

Nous avons assisté ces dernières années a une évolution du langage Java et de la JVM, à un rythme effréné. Les innovations n’ont pas arrêté de pleuvoir au fil des mois et des années. On est passé de Java 8 en 2014 à Java 17 en 2021 (il y a quelques jours). Le langage Java a évolué avec des nouvelles constructions, optimisations et modernisations. La JVM a également eu sa part d’évolutions, notamment en termes de gestion de mémoire et de GC. On peut être d’accord ou pas, mais l’innovation la plus importante ces dernières années est bel est bien l’arrivée de Graal VM. Cette innovation est à la base de nombreuses technologies qui ont suivi. C’est ainsi, avec beaucoup de succès, que nous avons vu apparaître de nouveaux frameworks dits natifs comme Micronaut, Quarkus, Helidon, et dernièrement Spring Native.

Dans cet article et dans ceux qui vont suivre, nous allons essayer de faire le tour d’horizon de cet ensemble de technologies issues de ou induites par GraalVM. Nous allons nous intéresser dans ce premier article aux bases : GraalVM.

Qu’est ce que GraalVM ?

GraalVM est un runtime (Environnement d’exécution) haute performance conçu pour accélérer l’exécution des applications écrites en Java et les langages de la JVM. Il peut également exécuter des applications écrites en JavaScript; Python, Ruby … Mieux encore, GraalVM permet l’interopérabilité entre ces langages au sein du même runtime.

En plus du runtime, GraalVM apporte également plusieurs nouveautés, nous nous intéressons dans cet article aux trois points les plus importants : Un compilateur JIT performant, La compilation AOT et l’aspect Polyglot.

 

 

Un compilateur JIT performant

Un mot sur le compilateur JIT (Just-In-Time). Au démarrage, une application Java (compilée avec javac) s’exécute en mode interprété (Bytecode >> Code machine). Au fur et à mesure de l’exécution, la JVM utilise le compilateur JIT pour compiler en natif les sections de code les plus utilisées, plutôt que de les interpréter perpétuellement. Ce qui, sur de longues exécutions, donne des performances proches d’une exécution native (Ex. C++).

 

Écrit en C++, le compilateur JIT de Hotspot est extrêmement complexe. Au vu de son importance dans la JVM, de sa complexité et de sa sensibilité, très peu de modifications et d’améliorations ont été apportées à celui-ci (risque de crash / mémoire). C’est ainsi qu’un nouveau projet de compilateur JIT écrit en Java, nommé GraalVM, a été lancé par Oracle.

GraalVM Compiler. C’est un compilateur JIT haute performance écrit en Java. Il y a deux principaux avantages à l’écrire en Java :

  1. La sûreté (Safety). En effet, en Java nous avons une meilleure gestion d’erreurs, à travers les exceptions, de plus la gestion de la mémoire est beaucoup plus simple et plus sûre.
  2. Il peut produire une version JIT de lui même, et peut donc s’optimiser en cours d’exécution !

De plus, le compilateur GraalVM inclut des dizaines d’algorithmes d’optimisation. Les optimisations sont d’autant plus importantes quand il s’agit de programmes / applications avec un niveau d’abstraction élevé : Streams, Lambdas … Par contre les programmes avec un niveau d’abstraction moins élevé ou faisant beaucoup de I/O, sont moins susceptibles d’être optimisés avec le compilateur JIT de GraalVM.

Des exemples sont présents sur le site officiel de GraalVM montrant les gains qui peuvent être constatés sur certains types d’applications.

 

Compilation Ahead Of Time

Motivations. Traditionnellement, les applications Java sont compilées vers du bytecode (un langage intermédiaire) qui est ensuite interprété lors de l’exécution dans la JVM. Nous avons vu plus haut le mécanisme d’optimisation grâce au JIT Compiler. L’exécution d’un code interprété est très lente en comparaison avec une exécution d’un code natif (exemple C++), surtout au WarmUP, i.e. au démarrage, avant l’optimisation induite par le JIT. De plus, la JVM occupe une part considérable en mémoire.

De nos jours, avec l’avènement des micro-services et du serverless, de nouvelles contraintes sont apparues :

  1. Les applications doivent démarrer de plus en plus vite, vu les mises à jour et les déploiements fréquents. Cela vaut encore plus pour les fonctions serverless, voir le cold start.
  2. Les applications / micro-services, au vu de leur prolifération, doivent consommer de moins en moins de mémoire (memory footprint).

Pour répondre à ces contraintes, GraalVM arrive avec Native Image : un compilateur Ahead-Of-Time qui compile le code Java en exécutable natif, qui s’exécute indépendamment de la JVM.

Comment ça fonctionne ? GraalVM Native Image fait une analyse statique de tout le code (classes) de l’application et de ses dépendances. Ainsi il détermine toutes les classes et méthodes qui seront atteignables durant l’exécution. A partir de là, “Native Image” compile, vers du code natif, toutes les sections atteignables, pour au final générer un fichier exécutable unique.

L’exécutable final va s’exécuter directement sur l’OS et pas sur la JVM. Par ailleurs, cet exécutable inclut également les composants nécessaires au runtime comme le thread scheduler et le garbage collector. Ces composants de base sont appelés Substrate VM.

Intérêt de l’AOT. Comparé aux applications Java qui s’exécutent sur la JVM, les images natives sont plus légères, plus rapides au démarrage, et consomment moins de mémoire. Elles sont idéalement taillées pour les micro-services et le serverless.

Avec les images natives, les frais d’hébergement baissent significativement, car nécessitant moins de mémoire. Les caractéristiques des applications produites par Graal Native Image (notamment le démarrage rapide en quelques dizaines de millisecondes) en font des candidats idéaux pour les micro-services, les fonctions (serverless) et les services orchestrés.

Native Image, avantages et inconvénients. Comme nous l’avons vu précédemment, Native Image procure des avantages, mais au prix de certains inconvénients. Notamment :

  • Le temps de build est extrêmement lent ;
  • Absence d’instrumentation JMX ;
  • Impossible d’avoir un Heap Dump ;
  • Beaucoup de frameworks ne sont pas supportés à cause de la réflexion ;
  • Moins efficace pour les Heaps importants avec de gros débits. En effet, uniquement le SerialGC est disponible pour la version Community. A noter que le G1 est disponible dans la version Enterprise de GraalVM.
  • Code machine moins optimisé que celui produit par JIT.

Y a-t-il un intérêt à écrire encore des applications qui tournent sur une JVM ? La réponse est : OUI, il y en a plus d’un :

  • La JVM optimise le code pendant l’exécution (grâce au JIT), cette intelligence n’est pas possible dans les applications compilées avec GraalVm Native Image.
  • Le temps de build d’une application native est extrêmement lent comparé au build d’un JAR.
  • Dans les applications natives, nous perdons une partie de la puissance de la JVM: l’instrumentation (JMX), plus d’accès au heap dump et thread dump …
  • Une JVM convient mieux au Heaps importants qu’un exécutable produit par Native Image.

Programmation polyglotte

GraalVM permet aux développeurs d’écrire des applications polyglottes, c’est -à -dire, dans une même application, nous pouvons avoir du Java, du R et du Javascript !

Cela est possible grâce à Truffle. Ce dernier est un outil permettant d’écrire des interpréteurs de langage de programmation. Ainsi l’on peut exécuter du Ruby, Javascript, R … dans une même JVM grâce aux interpréteurs de ces langages écrits en utilisant Truffle.

Comment se passe ce mix des langages dans la réalité ? En fait, tous les interpretteurs de ces langages disposent d’une API permettant d’interagir avec les autres langages supportés.

Il existe 2 façon d’être polyglotte sur GraalVM :

  1. Exécuter le code des autres langages à partir de programmes Java ;
  2. Accéder aux fonctionnalités des autres langages à partir d’un langage supporté par GraalVM.

La première option était déjà possible d’une certaine façon, avec JNI. La seconde option ci-dessus est complètement nouvelle.

Mais quel intérêt d’utiliser plusieurs langages pour écrire son programme ou son application? La réponse est simple : il existe des fonctionnalités, des librairies, des constructions, qui sont possibles dans un langage et pas dans un autre, l’idée étant d’utiliser les possibilités d’un autre langage pour pallier aux carences d’un autre.

Un exemple très intéressant, illustré ici, montre comment utiliser du code Java dans un programme R. Dans cet article, il est question de lire des données à partir de NEO4J dans un programme écrit en R. En effet, il n’existe pas de driver NEO4J pour le langage R. Grâce à GraalVM et Truffle, il est possible d’utiliser NEO4J à partir d’un programme R, en utilisant le driver Java de NEO4J !

Conclusion

Nous avons essayé, de façon simplifiée, dans cet article d’introduire GraalVM, et les nouvelles fonctionnalités apportées par ce projet. Ces apports sont considérables, et permettent à Java de rester au top, et constituer une alternative de choix pour les nouveaux usages en termes de micro-services, de serverless, et d’interopérabilité.

Software Engineer & CEO @TechInstinct