Introduction
GraalVM revolutionizes Java development in 2026 by compiling your code into native executables without the JVM. Imagine a microservice that starts in 10 ms instead of 2 seconds, uses 90% less memory, and better withstands denial-of-service attacks. This advanced tutorial targets senior developers: we start with a clean install to build a realistic Maven app with JSON parsing, error handling, and file I/O. You'll learn to generate optimized native images, configure the profiling agent for dynamic reflections, and benchmark against the classic JVM. By the end, your cloud or edge deployments will be 5-10x more efficient. Based on GraalVM Community Edition 22.x, compatible with Oracle JDK 21+. (128 words)
Prerequisites
- x64 Linux/macOS/Windows machine with at least 8 GB RAM
- Maven 3.9+ installed (
mvn --version) - Advanced knowledge of Java 21, reflections, and build tools
- Git for optional example clones
- Benchmark tools like
hyperfine(Linux/macOS)
Installing GraalVM
#!/bin/bash
# Download GraalVM CE 22.3.3 for Java 21 (x64 Linux/macOS adaptable)
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
# Verification
java -version
graalvmVersion=$(java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -version 2>&1 | grep GraalVM)
echo "GraalVM installed: $graalvmVersion"
# For macOS, use brew install --cask graalvm/tap/graalvm22-ee-java17 && /opt/homebrew/opt/graalvm/bin/gu install native-imageThis script downloads and configures GraalVM Community for Linux x64, installing the essential native-image component for native compilation. gu is the GraalVM Universe tool for extensions. On macOS, adapt with Homebrew. Always verify with native-image --version; avoid unsupported JDK versions like 17 to prevent linkage failures.
Basic Maven Project
Create a simple Maven project to test JVM vs. native. We implement a CLI app that parses JSON, calculates a sum, and writes a log file. This highlights classic pitfalls like missing reflections.
Basic POM File
<?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>This POM sets up Java 21 with Jackson for JSON. No GraalVM plugin yet: compile to a standard JAR first. Jackson triggers dynamic reflections, tripping up naive native images. Always use GraalVM-compatible versions from the registry.
Main Java Application
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);
}
}This app reads data.json (create it with [{ "value": 1, "name": "item1" }, { "value": 2, "name": "item2" }]), parses with Jackson (reflections), sums values, and logs. Real-world example exposing I/O, streams, and databind. Compile with mvn compile; run mvn exec:java -Dexec.mainClass="com.example.Main". JVM time ~50ms.
JAR Compilation and JVM Test
Create data.json in the root folder:
[{"value":10,"name":"prod"},{"value":20,"name":"test"},{"value":30,"name":"dev"}]
Run mvn clean package for the JAR. Then java -jar target/graalvm-demo-1.0-SNAPSHOT.jar. Check result.log: sum=60. Benchmark: hyperfine 'java -jar target/*.jar'.
Add GraalVM Maven Plugin
<?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>Adds the official GraalVM native-maven-plugin for mvn native:compile. enables AOT analysis. First run will fail due to Jackson (reflections): that's expected; we'll profile next. Initial build time ~2min.
Profiling with Native Image Agent
#!/bin/bash
# Generate config.json for dynamic reflections
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/"
# Check generated files:
# - reflect-config.json (Jackson)
# - resource-config.json (data.json)
# - proxy-config.json (if proxies)
ls src/main/resources/META-INF/native-image/The -agentlib:native-image-agent traces reflections, resources, and proxies at JVM runtime. Run your app once to auto-generate configs in META-INF/native-image/. Essential for 95% of third-party libs; rerun multiple scenarios (errors, branches) for full coverage.
First Working Native Image
After profiling, run mvn clean native:compile -Dnative-image.docker-build=false. Get target/graalvm-demo-1.0-SNAPSHOT-runner. Run ./target/graalvm-demo-1.0-SNAPSHOT-runner: same output, but startup <10ms! Benchmark: hyperfine './target/-runner' 'java -jar target/.jar' – 5-10x gain.
Advanced Optimizations in POM
<?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>Add for mainClass, buildArgs like --no-fallback (crash if non-AOT), -H:+ReportExceptionStackTraces for debugging. ConfigurationFileDirectories points to agent configs. Reduces binary size by 20-30% and speeds up builds.
Full Benchmark Script
#!/bin/bash
# Install hyperfine if needed: cargo install hyperfine
JAR="target/graalvm-demo-1.0-SNAPSHOT.jar"
NATIVE="target/graalvm-demo"
# Cold time (first run)
echo "Cold benchmark:"
hyperfine --warmup 3 'java -jar $JAR' './$NATIVE'
# Peak memory RSS (Linux)
echo "Peak memory (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'This script compares JVM vs. native with hyperfine: cold startup time, RSS memory, multiple runs. Expect JVM ~200ms / 150MB vs. native ~8ms / 15MB. --export-json for graphs. On edge (Kubernetes), see x10 scaling gains.
Best Practices
- Profile exhaustively: Cover all paths (exceptions, conditions) with the agent before final build.
- Minimize footprint: Use
-H:IncludeResources="data\\.json"precisely; analyze withnative-image --expert-options. - Test AOT-only: Always use
--no-fallbackin production to catch misses. - Integrate CI/CD: Docker multi-stage for reproducible builds:
FROM ghcr.io/graalvm/native-image-community:22-ol9. - Choose compatible libs: Check GraalVM Reachability Metadata for auto-config.
Common Errors to Avoid
- Unprofiled reflections:
ClassNotFoundExceptionat native runtime – fix: agent + native mode unit tests. - Missing resources: Files like
data.jsonignored – addresource-config.jsonor-H:IncludeResources. - JDK experimental flags: Forget
JAVA_HOMEGraalVM – builds fail withunsupported class version. - Docker builds without volume:
native-imageneeds 8GB+ RAM; limit threads with-H:MaxHeapSize=4g.
Next Steps
- Official docs: GraalVM Native Image Guide
- Advanced examples: Spring Native
- Tools: Dynamic
native-image-agent,trace-agentfor threads. - Learni Dev Training: Master GraalVM, Quarkus, and Micronaut in certified bootcamps.