Skip to content
Learni
View all tutorials
Outils de développement

How to Master GraalVM for Native Apps in 2026

Lire en français

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 via gu install native-image)

Installing GraalVM

install-graalvm.sh
#!/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

HelloGraal.java
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

build-native.sh
#!/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 MB

These 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

ReflectiveApp.java
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

reflect-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

build-reflective.sh
#!/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-app

Integrates 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

pom.xml
<?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

GraalSpringApplication.java
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

build-spring-native.sh
#!/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-spring

Compiles 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:+PrintClassInitialization and 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 hyperfine or wrk; integrate CI with GitHub Actions.
  • Optimized polyglot: Bind JS/Python via Context with --language:js.
  • Multi-stage Docker: Build native in containers for portability.

Common Errors to Avoid

  • Missing configs: ClassNotFound or NoSuchMethod → Always trace or declare reflection/JNI/resources.
  • Incomplete flags: Without --enable-URL-protocols=http,https or preview features (--enable-preview), networking fails.
  • Inadapted tests: JUnit assumes JVM; use @NativeTest or skip.
  • Excessive build memory: native-image eats 8+ GB → Increase heap (-Xmx16g) and use -H:+ReportExceptionStackTraces.

Next Steps

How to Master GraalVM Native Apps in 2026 | Learni