Introduction
GraalVM, developed by Oracle, is a revolutionary toolkit for running and compiling polyglot applications (Java, JavaScript, Python, etc.) into native binaries. Unlike the traditional JVM, which interprets bytecode on the fly, GraalVM generates native executables via Ahead-Of-Time (AOT) compilation, delivering millisecond startups, 50-90% reduced memory usage, and 2-5x better performance on CPU-bound workloads.
Why adopt it in 2026? Containers and serverless demand lightweight apps: a JVM JAR weighs 100+ MB and starts in seconds, while a GraalVM native image is under 50 MB and boots in under 100 ms. Ideal for microservices, edge computing, or CLI tools. This advanced tutorial guides you step by step: from installation to advanced optimizations handling reflection, resources, and profiling. By the end, you'll compile a production-ready native Spring Boot app.
Prerequisites
- GraalVM Community Edition 22+ (or Oracle GraalVM for pro features)
- JDK 17+ installed (GraalVM includes its own JDK)
- Maven 3.9+ or Gradle 8+ for builds
- Linux/macOS (Windows supported but Linux optimized for native)
- Advanced knowledge of Java, reflection, and JVM internals
- Tools:
native-image(installed viagu install native-image)
Installing GraalVM
#!/bin/bash
# Download GraalVM CE 22.3.0 (adapt the version)
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-22.0.1/graalvm-community-jdk-22.0.1_linux-x64_bin.tar.gz
tar -xzf graalvm-community-jdk-22.0.1_linux-x64_bin.tar.gz
export JAVA_HOME=$PWD/graalvm-community-openjdk-22.0.1+8.1
export PATH=$JAVA_HOME/bin:$PATH
gu install native-image
gu install js # Optional: for polyglot JS
echo "GraalVM installed. Check: native-image --version"This script downloads, extracts, and configures GraalVM as the default JDK, installing the essential native-image AOT compiler. Use gu (Graal Updater) to add components like JS or Python. Pitfall: Forget export JAVA_HOME/PATH and native-image won't be found; always test with java --version and native-image --version.
Your First Simple Native Image
Let's start with a Java Hello World to validate the installation. We'll create a standalone app, compile it native, and compare startup time/memory vs JVM.
Java Hello World Application
public class HelloGraal {
public static void main(String[] args) {
System.out.println("Hello from GraalVM Native Image!");
for (int i = 0; i < 5; i++) {
System.out.println("Iteration " + i + ": Performance native!");
}
}
}This simple class loops and prints to simulate a workload. It's 100% static with no reflection, perfect for a first native test. Compile with javac HelloGraal.java then native-image -o hellograal HelloGraal: the ./hellograal executable starts instantly without a JVM.
Compiling to Native Image
#!/bin/bash
javac -cp . HelloGraal.java
# Build native image (optimized)
native-image \
--no-fallback \
--enable-https \
--enable-http \
--allow-incomplete-classpath \
-O \
-march=native \
-o hellograal HelloGraal
# Test
./hellograal
time ./hellograal # Measure perf
du -h hellograal # Size ~10-20 MBThese flags enable optimizations (-O), HTTPS/HTTP for networking, and --no-fallback forces pure native (errors if impossible). --allow-incomplete-classpath tolerates minor misses. Result: 15 MB binary, boot under 10 ms vs JVM 200 ms+.
Handling Reflection and Resources
Key challenge: GraalVM performs static analysis; reflection and resources (files, dynamic classes) require explicit JSON configs. Without them, you get runtime errors like ClassNotFoundException. For real apps, configure reflect-config.json, resource-config.json, and jni-config.json.
Java App with Reflection
import java.lang.reflect.Method;
public class ReflectiveApp {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("java.lang.String");
Method method = clazz.getMethod("toUpperCase");
String result = (String) method.invoke("hello graal");
System.out.println("Reflected: " + result);
}
public static void dynamicMethod() {
System.out.println("Méthode dynamique appelée!");
}
}This app uses Class.forName and getMethod to dynamically invoke toUpperCase. Without reflection config, native-image fails static analysis. Add reflect-config.json to whitelist these classes/methods.
Reflection Config JSON
[
{
"name": "java.lang.String",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
},
{
"name": "ReflectiveApp",
"methods": [
{"name": "dynamicMethod", "parameterTypes": []}
]
}
]This JSON grants full access to String and specific dynamicMethod. Pass to native-image via -H:ReflectionConfigurationFiles=reflect-config.json. Tip: Use jdeps or tracing agent for auto-generation: native-image -H:+PrintClassInitialization ....
Build with Reflection Config
#!/bin/bash
javac ReflectiveApp.java
native-image \
--no-fallback \
-H:+ReportExceptionStackTraces \
-H:ReflectionConfigurationFiles=reflect-config.json \
-H:ResourceConfigurationFiles=resource-config.json \
-O \
-o reflective-app ReflectiveApp
./reflective-appIntegrates the configs; -H:+ReportExceptionStackTraces aids runtime debugging. Add resource-config.json for included files (e.g., {"resources": {"includes": [{"pattern": ".*\.properties"}]}}). Binary now handles reflection without crashing.
Native Spring Boot with GraalVM
For frameworks like Spring Boot, use the GraalVM Maven plugin. Full example: REST API exposing endpoints with JSON and simulated DB (H2).
pom.xml for Spring Boot Native
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>graal-spring</artifactId>
<version>1.0</version>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
<graalvm.version>22.3.0</graalvm.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.2</version>
<extensions>true</extensions>
<executions>
<execution>
<goals><goal>native-compile</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>This pom.xml includes Spring Web + H2, plus the native-maven-plugin for mvn -Pnative native:compile. It auto-handles much of Spring's reflection (via hints). Final size ~40 MB, boot <200 ms.
Spring Boot REST App
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class GraalSpringApplication {
public static void main(String[] args) {
SpringApplication.run(GraalSpringApplication.class, args);
}
}
@RestController
class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello Native Spring from GraalVM!";
}
}Minimal app with /hello endpoint. Place in src/main/java/com/example/. Build: mvn -Pnative package produces target/graal-spring executable. Test: ./target/graal-spring ; curl localhost:8080/hello. Perfect for microservices.
Build and Test Spring Native
#!/bin/bash
# Ensure you're in the Maven project
mvn clean package -Pnative \
-DskipTests \
--batch-mode
# Run
./target/graal-spring
# In another terminal
time curl -s http://localhost:8080/hello | cat
du -h target/graal-springCompiles to native via Maven profile. -DskipTests avoids tracing issues in native tests. Result: API responds in <50 ms, memory <100 MB vs JVM 500+ MB.
Best Practices
- Profile before AOT: Use
-H:+PrintClassInitializationand tracing agent (java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/) for auto-configs. - Minimize footprint: Avoid
ThreadLocal,finalizers; use static init only. - Test prod-like: Compare CPU/memory with
hyperfineorwrk; integrate CI with GitHub Actions. - Optimized polyglot: Bind JS/Python via
Contextwith--language:js. - Multi-stage Docker: Build native in containers for portability.
Common Errors to Avoid
- Missing configs:
ClassNotFoundorNoSuchMethod→ Always trace or declare reflection/JNI/resources. - Incomplete flags: Without
--enable-URL-protocols=http,httpsor preview features (--enable-preview), networking fails. - Inadapted tests: JUnit assumes JVM; use
@NativeTestor skip. - Excessive build memory:
native-imageeats 8+ GB → Increase heap (-Xmx16g) and use-H:+ReportExceptionStackTraces.
Next Steps
- Official docs: GraalVM Reference
- Profiling: Integrate
async-profilerwith native. - Advanced examples: Spring Native GitHub
- Expert training: Check out our advanced Java trainings at Learni to master GraalVM in enterprise.