Découvrez le JAVA Natif avec GraalVM

Depuis quelques années, les usages des programmes informatiques évoluent, les services cloud se multiplient, les langages offrent toujours plus de fonctionnalités.
Dans l’éco-système JAVA, beaucoup de choses ont évolués. Les entreprises commencent enfin à sortir des versions 8 et inférieures pour profiter des apports des dernières versions. Avec l’arrivée du JAVA Natif et de GraalVM, c’est une autre facette du JAVA qui est mise à disposition des projets. Découvrons ensemble les apports et les points de vigilance du Java Natif avec GraalVM.

Rappel du fonctionnement de la compilation d’une application JAVA

Une application JAVA n’est pas directement compréhensible par le processeur d’un ordinateur. Elle a besoin d’un intermédiaire pour se faire comprendre : La JVM ou Java Virtual Machine.

Cette JVM va s’adapter à la machine (Windows, macOS, Linux, processeur x86, ARM, Power…). C’est ce qui va permettre de créer un seul paquet applicatif compatible avec une grande diversité d’ordinateur.

Compilation JIT

Le code source (vos fichiers .java) sont compilé en « bytecode » (vos fichiers au format .class). C’est ce bytecode qui sera ensuite interprété par la JVM afin de faire tourner votre application.

Des performances en retrait

Le JAVA est aussi connu pour ses performances moindre par rapport à d’autres langages comme le C++, le Go ou tout autre langage directement interpretable par un processeur.

Le JAVA à la traine

Aujourd’hui, les principaux reproches fait aux applications JAVA sont :

  • Un temps de démarrage long, dû au démarrage de la JVM, au chargement des classes…
  • Une empreinte mémoire élevée, dû à la JVM, au système Garbage Collector
  • Une utilisation du processeur supérieure, car le code n’est pas directement interpretable

L’arrivée du cloud

Les services cloud, présents depuis plusieurs années maintenant ont démocratisé un nouveau fonctionnement économique : on paie ce qu’on consomme.

Et dans la continuité de ce modèle économique, les services de type « serverless » ou « scale at 0 » ont également gagné en popularité. Ces services vous proposent de ne lancer votre application que lorsqu’elle est accédée par un utilisateur. Ainsi ils vous permettent d’économiser dans les moments ou elle ne l’est pas.

L’optimisation des performances devient donc un enjeu économique encore plus important.

Le Java Natif GraalVM à la rescousse

Le JAVA tel que vous le connaissez ne semble donc pas vraiment bien adapté à ces usages cloud ou serverless. En effet, une application qui met 5 secondes à démarrer lorsque quelqu’un souhaite y accéder est un délais bien trop élevé.

GraalVM est un JDK avancé développé par Oracle permettant à votre application de

  • Moins consommer en ressource
  • Démarrer plus rapidement
  • Avoir une moins grande surface d’attaque

Une histoire de moment de compilation

Just In Time compilation (JIT)

Une application JAVA n’est pas directement interprétée par le processeur, la JVM fait intermédiaire pour la ‘traduire ». La JVM va donc « traduire » en temps réel (Just In Time) le bytecode Java en code machine.

Pour ce faire, elle va s’appuyer sur des compilateurs de différents niveau (C1 et C2). Ils vont optimiser la compilation du code en fonction de l’utilisation de l’application. C’est ce qui va permettre a une application JAVA d’atteindre son pic de performance après une certaine durée d’utilisation.

Si du code compilé n’est plus nécessaire, il sera « dé-optimisé »

Ahead of Time compilation (AoT)

Les fameuses étapes d’optimisations faites par la JVM dans le cas d’une compilation JIT vont être faite en amont, au moment de la compilation du code source de l’application.

Ce cache de « code optimisé » sera finalement construit dès le début et intégré au livrable final, afin de ne plus avoir avec le traitement de compilation temps réel pendant le fonctionnement de l’application.

Quelques points de vigilance cependant :

  • Cela peut nécessiter l’adaptation des librairies tierces que vous utilisez
  • Tout ce qui n’est pas détecté dans la compilation AoT sera exclu du paquet applicatif final
  • Les performances de l’application compilé en AoT sont « figées » et peuvent être inférieures à celles d’une application avec compilation JIT

Au final, le parti pris est de perdre du temps à la compilation pour le gagner à l’exécution. C’est ce qui est recherché dans le cadre d’un cas d’utilisation ou le temps de démarrage est critique par exemple.

Comparaison des forces et faiblesses entre JIT et AOT

Au final, chaque mode de compilation a ses avantages et ses inconvénients. Il est crucial de bien comprendre le cas d’utilisation avant de faire un choix.

Quels gains en Java Natif avec GraalVM ?

Gains de performance d’une application Java Native GraalVM

Quelques exemples des performances atteintes par des applications Java Natif GraalVM comparées à des applications « standard ».

Les deux captures ci dessous ont été faite sur une application Springboot d’un projet personnel. Celle ci possède les caractéristiques suivantes :

  • Exposition de endpoint REST
  • Utilisation du framework de templating HTML Thymleaf
  • Configuration OAuth2 avec Keycloak (pour le front intégré et la validation des Bearer via API)
  • Spring session avec Redis
  • Spring Security
  • BDD H2 avec Liquibase lancé au démarrage
  • Lib tierce pour interagir avec l’API Gitlab
  • Webjars pour la partie Thymleaf

Le tableau ci dessous concerne une application Springboot exposant un endpoint REST.

Le premier élément qui saute aux yeux est le temps de démarrage. Selon les exemples, on est entre 20x et 50x plus rapide avec des démarrages sous la seconde voir en dixième/centièmes de seconde ! C’est exactement ce qui est recherché dans le cadre d’application serverless.

La consommation CPU et mémoire est elle aussi améliorée avec une division par 3 ce qui reste très interessant pour faire des économies lorsqu’on paie à l’usage.

La promesse est tenu, le gain est important et peut permettre de nouveaux cas d’utilisation avec Java là où un autre langage aurait pu être préférés auparavant.

Quels impacts pour les équipes ?

Des durées de compilations grandement augmentées

Comme nous l’avons vu avant, la compilation AoT réalise des traitements au moment de la compilation du code afin d’éviter de les réaliser au moment de l’exécution de l’application.

Le premier impact est donc une durée de compilation décuplée.

Durée de compilation

Pour une application compilée en moins de 10 secondes en mode « standard », la compilation AoT peut atteindre 4 à 5mn sur une machine correctement équipée à 7 minutes sur une VM avec suffisamment de RAM. Si la machine qui compile ne possède pas assez de mémoire, la compilation peut échouer.

Consommation mémoire

Cette compilation n’est pas seulement longue, elle est aussi très gourmande en ressource. Des machines avec 8Go de RAM au minimum sont nécessaires. Mais pour assurer un confort il vaut mieux viser 16Go de RAM au minimum.

Le paquet applicatif devient spécialisé pour un type de machine

Là où la JVM permet de faire tourner un même paquet applicatif sur une multitude de machine, la compilation native va au contraire spécialiser l’application sur le type de machine ayant servi à la construire.

Si vous compilez une app Java native GraalVM sur Linux, elle ne pourra tourner que sur une machine Linux. Mais la spécialisation ne s’arrête pas là. Car l’architecture du processeur est aussi un élément déterminant.

Une application compilée sur une machine Linux avec processeur x86 ne pourra pas tourner sur une machine Linux avec processeur ARM (sans sur-couche d’émulation x86).

Cette nuance devient importante lorsqu’on utilise la conteneurisation. En effet, la conteneurisation passe par une émulation Linux dans les coulisses sur macOS et Windows par exemple.

Il devient donc impossible de créer le paquet applicatif sur un Mac avec architecture ARM et de le « copier » dans une image de conteneur, car vous vous retrouvez avec un conteneur Linux embarquant un binaire mac qui ne sera pas exécutable.

Une compilation « multi-stage » sera obligatoire, c’est-à-dire que vous devrez utiliser un conteneur docker pour compiler afin d’obtenir un binaire Linux. Ce binaire Linux pourra ensuite être lancé dans un conteneur Linux.

Le compilateur natif élimine ce qu’il ne voit pas

En compilation AoT, le compilateur va parcourir l’ensemble des chemins d’exécution de votre application afin d’éliminer ce qui n’est pas nécessaire. Cela va permettre de réduire le volume de votre application et aussi de réduire la surface d’attaque en embarquant que le nécessaire. Le point positif est que cela se traduira par une réduction de vulnérabilités trouvées par un scanner comme Trivy – Un scanner de vulnérabilité simple et rapide

Cependant, beaucoup de framework Java reposent sur des concepts dynamiques qui ne sont pas encore bien gérés par GraalVM comme :

  • Annotations dynamiques Spring (@Profile par ex)
  • Reflexion, Serialisation
  • Lecture de fichier du classpath (la notion de classpath n’existe plus en natif)

Il est donc nécessaire « d’aider » le compilateur en lui indiquant via des fichiers de configuration les classes à utiliser meme s’il ne les détecte pas.

Oracle met à disposition l’outil « Metadata reachability » qui est un agent qui se lance en même temps que votre application et qui détecte les classes et autres objets utilisés. Ces elements sont ensuite repris lors de la compilation AoT.

Les binaires Java Natif GraalVM ne sont pas tous égaux

L’utilisation de binaires natifs implique la connaissance de concepts connu des developpeurs sur des langages natifs comme le C++ mais pas forcément des développeurs Java.

Un binaire natif va devoir s’appuyer sur des bibliothèques système afin de fonctionner (gestion des entrées/sorties, gestion de la mémoire, utilisation de l’affichage graphique…)

Ces librairies peuvent être portées par le système d’exploitation ou bien être directement intégrées au binaire natif.

Les applications sont donc liées (linkées) aux bibliothèques

  • Link « dynamique » : C’est le mode par défaut, le binaire natif s’appuie sur des librairies extérieures pour fonctionner.
  • Link « mostly static » ou « presque statique » : le binaire natif embarque toutes les librairies nécessaires SAUF la librairie appelée « LibC » qui contient les fonctions bas niveau (entrée/sortie, gestion mémoire…)
  • Link « static » ou statique : Toutes les librairies sont embarquées dans le binaire cependant, LibC est remplacée par une autre librairie appelée Musl
Link dynamique, Link Mostly Static, Link Static

Comme pour tout questionnement sur les avantages et inconvénients de mutualiser des choses, les impacts sont les suivants :

Lib mutualisées :

  • Avantage : Votre binaire est moins lourd, plusieurs binaires peuvent utiliser une même lib.
  • Inconvénient : Difficile de mettre à jour une librairie partagée sans casser les binaires qui s’appuient dessus

Lib embarquées :

  • Avantage : Autonomie complète sur le binaire, aucune dépendances
  • Inconvénient : Binaire plus lourd et si une mise à jour de la lib doit être faite il faut recompiler le binaire.

LibC et Musl

La « lib C » est une bibliothèque de fonctions C de base qui fournit des fonctionnalités de bas niveau telles que l’entrée/sortie, la gestion de la mémoire et la manipulation de chaînes. 

La lib « musl » est une alternative à la lib c. Elle est plus légère et plus rapide que la lib c, mais ne prend pas en charge toutes les fonctionnalités de la lib c. Lors de la recherche de performance et légèreté la lib musl peut être une option intéressante.

Cette nuance est importante, car certaines distributions linux n’ont pas la LibC et donc ne sont pas compatibles avec des binaires dynamiques ou presque statiques générés par GraalVM (exemple les distributions Alpine)

L’avantage d’un binaire statique Linux est qu’il aura une portabilité maximale étant compatible avec tous les variantes Linux. Cependant les fonctions de la lib Musl sont moindre que la lib C donc selon les cas d’utilisation elle ne serait pas envisageable.

Pour conclure

La compilation en Java Natif GraalVM est idéale dans les cas d’utilisation

  • De petits périmètres type Microservice
  • Avec peu de dépendances tierces
  • Facile à tester

Cette nouvelle fonctionnalités offerte à la communauté JAVA amène un gap de performance crucial selon les cas d’utilisation.

Il faut être vigilant à avoir un patrimoine de test suffisant afin de maximiser les passages dans les chemins d’exécutions et collecter avec l’agent de collecte de méta données le maximum d’information

Au quotidien, afin d’éviter de perdre 5mn à chaque compilation, il est possible d’utilisation un mode hybride « JVM + AoT ». Cela permet au final d’utiliser les données AoT au RUN tout en ayant une compilation rapide

Il faut être vigilant aux ressources disponible sur les postes de développement mais aussi les chaines CI/CD ainsi qu’aux architectures qui doivent être au plus proche de la production

La compilation native est disponible sur Springboot, Quarkus, Micronaut de manière aisée.

Quelques liens utiles

Lors de la rédaction de cet articles j’ai pu trouver beaucoup d’informations sur les sites ci dessous. Je vous encourage à aller les consulter.

https://www.graalvm.org/latest/reference-manual/native-image/

https://www.graalvm.org/22.0/reference-manual/native-image/StaticImages/

https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.introducing-graalvm-native-images

https://www.cesarsotovalero.net/blog/aot-vs-jit-compilation-in-java.html

https://developers.redhat.com/articles/2021/06/23/how-jit-compiler-boosts-java-performance-openjdk#

https://medium.com/graalvm/graalvm-community-survey-2022-results-328d0404d36e

https://medium.com/graalvm/graalvm-in-2023-a-year-in-review-60f7f0635671

https://tech-stack.com/blog/using-graalvm-in-a-real-world-scenario-techstacks-experience/

Print Friendly, PDF & Email

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.