Skip to content
Learni
View all tutorials
Java

How to Optimize Java with GraalVM in 2026

Lire en français

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

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

This 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

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>

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

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);
    }
}

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

pom.xml (updated)
<?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. true 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

profile-agent.sh
#!/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

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

benchmark.sh
#!/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 with native-image --expert-options.
  • Test AOT-only: Always use --no-fallback in 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: ClassNotFoundException at native runtime – fix: agent + native mode unit tests.
  • Missing resources: Files like data.json ignored – add resource-config.json or -H:IncludeResources.
  • JDK experimental flags: Forget JAVA_HOME GraalVM – builds fail with unsupported class version.
  • Docker builds without volume: native-image needs 8GB+ RAM; limit threads with -H:MaxHeapSize=4g.

Next Steps

How to Optimize Java with GraalVM in 2026 | Learni