Skip to content
Learni
View all tutorials
Java

Comment optimiser Java avec GraalVM en 2026

Introduction

GraalVM révolutionne le développement Java en 2026 en compilant votre code en exécutables natifs, sans JVM. Imaginez un microservice qui démarre en 10 ms au lieu de 2 secondes, consomme 90 % de mémoire en moins et résiste mieux aux attaques de déni de service. Ce tutoriel avancé cible les développeurs seniors : nous partons d'une installation propre pour créer une app Maven réaliste avec parsing JSON, gestion d'erreurs et I/O fichiers. Vous apprendrez à générer des native images optimisées, à configurer l'agent de profiling pour les réflexions dynamiques, et à benchmarker contre la JVM classique. À la fin, vos déploiements cloud ou edge seront 5-10x plus efficaces. Basé sur GraalVM Community Edition 22.x, compatible Oracle JDK 21+. (128 mots)

Prérequis

  • Machine x64 Linux/macOS/Windows avec 8 GB RAM minimum
  • Maven 3.9+ installé (mvn --version)
  • Connaissances avancées en Java 21, reflections et build tools
  • Git pour cloner des exemples optionnels
  • Outils de benchmark comme hyperfine (Linux/macOS)

Installation de GraalVM

install-graalvm.sh
#!/bin/bash

# Téléchargement GraalVM CE 22.3.3 pour Java 21 (x64 Linux/macOS adaptables)
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-22.0.3/graalvm-community-jdk-22.0.3_linux-x64_bin.tar.gz

tar -xzf graalvm-community-jdk-22.0.3_linux-x64_bin.tar.gz

export JAVA_HOME=$PWD/graalvm-community-openjdk-22.0.3+7.1

export PATH=$JAVA_HOME/bin:$PATH

gu install native-image

# Vérification
java -version
graalvmVersion=$(java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -version 2>&1 | grep GraalVM)
echo "GraalVM installé: $graalvmVersion"

# Pour macOS, utilisez brew install --cask graalvm/tap/graalvm22-ee-java17 && /opt/homebrew/opt/graalvm/bin/gu install native-image

Ce script télécharge et configure GraalVM Community pour Linux x64, installe le composant native-image essentiel pour la compilation native. gu est l'outil GraalVM Universe pour les extensions. Sur macOS, adaptez avec Homebrew. Vérifiez toujours native-image --version pour confirmer ; évitez les versions JDK non supportées comme 17 pour éviter des échecs de linkage.

Premier projet Maven de base

Créez un projet Maven simple pour tester JVM vs native. Nous implémentons une app CLI qui parse un JSON, calcule une somme et écrit un fichier log. Cela expose les pièges classiques comme les réflexions manquantes.

Fichier POM de base

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>graalvm-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <graalvm.version>22.0.3</graalvm.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Ce POM configure Java 21 avec Jackson pour JSON. Pas encore de plugin GraalVM : on compile d'abord en JAR standard. Jackson déclenche des réflexions dynamiques, piégeant les native images naïves. Utilisez toujours des versions compatibles GraalVM listées sur le registre.

Application Java principale

src/main/java/com/example/Main.java
package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;

public class Main {
    public static class Item {
        public int value;
        public String name;
    }

    public static void main(String[] args) throws IOException {
        // Lire JSON d'un fichier
        String jsonContent = Files.readString(Paths.get("data.json"));
        ObjectMapper mapper = new ObjectMapper();
        List<Item> items = mapper.readValue(jsonContent, mapper.getTypeFactory().constructCollectionType(List.class, Item.class));

        // Calcul somme
        int sum = items.stream().mapToInt(i -> i.value).sum();

        // Écrire log
        try (FileWriter writer = new FileWriter("result.log")) {
            writer.write("Somme totale: " + sum + "\n");
        }

        System.out.println("Traitement terminé. Somme: " + sum);
    }
}

Cette app lit data.json (créez-le avec [{ "value": 1, "name": "item1" }, { "value": 2, "name": "item2" }]), parse avec Jackson (reflections), somme et log. Exemple concret exposant I/O, streams et databind. Compilez avec mvn compile ; exécutez mvn exec:java -Dexec.mainClass="com.example.Main". Temps JVM ~50ms.

Compilation en JAR et test JVM

Créez data.json dans le dossier racine :
``json
[{"value":10,"name":"prod"},{"value":20,"name":"test"},{"value":30,"name":"dev"}]
`

Exécutez mvn clean package pour JAR. Puis java -jar target/graalvm-demo-1.0-SNAPSHOT.jar. Vérifiez result.log : somme=60. Benchmark : hyperfine 'java -jar target/*.jar'`.

Ajouter plugin GraalVM Maven

pom.xml (mis à jour)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>graalvm-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <graalvm.version>22.0.3</graalvm.version>
        <native.maven.plugin.version>22.3</native.maven.plugin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native.maven.plugin.version}</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>native-compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Ajout du native-maven-plugin officiel GraalVM pour mvn native:compile. true active l'analyse AOT. Première exécution échouera à cause de Jackson (reflections) : c'est normal, on profile ensuite. Temps de build initial ~2min.

Profiling avec Native Image Agent

profile-agent.sh
#!/bin/bash

# Génère config.json pour reflections dynamiques
mvn clean compile exec:java \
  -Dexec.mainClass="com.example.Main" \
  -Dexec.args="-agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/"

# Vérifiez les fichiers générés :
# - reflect-config.json (Jackson)
# - resource-config.json (data.json)
# - proxy-config.json (si proxies)

ls src/main/resources/META-INF/native-image/

L'agent -agentlib:native-image-agent trace les réflexions, ressources et proxies à runtime JVM. Exécutez votre app une fois pour générer les configs auto dans META-INF/native-image/. C'est crucial pour 95% des libs tierces ; relancez plusieurs scénarios (erreurs, branches) pour couverture complète.

Première native image fonctionnelle

Après profiling, mvn clean native:compile -Dnative-image.docker-build=false. Obtenez target/graalvm-demo-1.0-SNAPSHOT-runner. Exécutez ./target/graalvm-demo-1.0-SNAPSHOT-runner : même sortie, mais démarrage <10ms ! Benchmark : hyperfine './target/-runner' 'java -jar target/.jar' – gain x5-10.

Optimisations avancées dans POM

pom.xml (optimisé)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>graalvm-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <graalvm.version>22.0.3</graalvm.version>
        <native.maven.plugin.version>22.3</native.maven.plugin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native.maven.plugin.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <mainClass>com.example.Main</mainClass>
                    <buildArgs>
                        <buildArg>-H:+ReportExceptionStackTraces</buildArg>
                        <buildArg>-H:ConfigurationFileDirectories=META-INF/native-image</buildArg>
                        <buildArg>--no-fallback</buildArg>
                        <buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
                    </buildArgs>
                    <imageName>graalvm-demo</imageName>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>native-compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Ajoutez pour mainClass, buildArgs comme --no-fallback (crash si non-AOT), -H:+ReportExceptionStackTraces pour debug. ConfigurationFileDirectories pointe vers vos configs agent. Réduit taille binaire de 20-30% et accélère build.

Benchmark script complet

benchmark.sh
#!/bin/bash

# Installez hyperfine si besoin: cargo install hyperfine

JAR="target/graalvm-demo-1.0-SNAPSHOT.jar"
NATIVE="target/graalvm-demo"

# Temps froid (premier run)
echo "Benchmark froid:"
hyperfine --warmup 3 'java -jar $JAR' './$NATIVE'

# Mémoire RSS (Linux)
echo "Mémoire peak (RSS kb):"
/usr/bin/time -f "%M" java -jar $JAR
/usr/bin/time -f "%M" ./$NATIVE

# CPU/RSS multiple runs
hyperfine --export-json bench.json --min-runs 20 'java -jar $JAR' './$NATIVE'

Ce script compare JVM vs native avec hyperfine : temps froid (startup), mémoire RSS, runs multiples. Attendez JVM ~200ms / 150MB vs native ~8ms / 15MB. --export-json pour graphs. Sur edge (Kubernetes), gains x10 en scaling.

Bonnes pratiques

  • Profilez exhaustivement : Couvrez tous les chemins (exceptions, conditions) avec l'agent avant build final.
  • Minimisez footprint : Utilisez -H:IncludeResources="data\\.json" précisément ; analysez avec native-image --expert-options.
  • Testez AOT-only : Toujours --no-fallback en prod pour détecter les manques.
  • Intégrez CI/CD : Docker multi-stage pour builds reproducibles : FROM ghcr.io/graalvm/native-image-community:22-ol9.
  • Choisissez libs compatibles : Vérifiez GraalVM Reachability Metadata pour auto-config.

Erreurs courantes à éviter

  • Reflections non profilées : ClassNotFoundException à runtime native – solution : agent + tests unitaires en mode native.
  • Ressources manquantes : Fichiers comme data.json ignorés – ajoutez resource-config.json ou -H:IncludeResources.
  • JDK experimental flags : Oubliez JAVA_HOME GraalVM – builds échouent avec unsupported class version.
  • Builds Docker sans volume : native-image nécessite 8GB+ RAM ; limitez threads avec -H:MaxHeapSize=4g.

Pour aller plus loin