GraalVM is a high-performance JDK distribution that accelerates Java applications while enabling native compilation. Native Image technology compiles Java applications ahead-of-time into standalone native executables.
1. GraalVM Overview
What is GraalVM?
- Universal VM: Supports multiple languages (Java, JavaScript, Python, R, Ruby)
- High Performance: Advanced JIT compiler with optimizations
- Native Image: AOT (Ahead-of-Time) compilation to native executables
- Polyglot Capabilities: Run multiple languages in same runtime
GraalVM Editions:
- Community Edition (CE): Free, open-source
- Enterprise Edition (EE): Commercial with additional optimizations
Key Benefits:
- Fast Startup: Milliseconds instead of seconds
- Low Memory Footage: Reduced RSS memory usage
- Instant Peak Performance: No warmup needed
- Container Friendly: Small Docker images
- Better Security: Reduced attack surface
2. Installation and Setup
Installing GraalVM
Using SDKMAN (Recommended)
# List available Java versions sdk list java # Install GraalVM sdk install java 22.3.1.r17-grl # Set as default sdk use java 22.3.1.r17-grl # Verify installation java -version native-image --version
Manual Installation
# Download from https://www.graalvm.org/downloads/ wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.1/graalvm-ce-java17-linux-amd64-22.3.1.tar.gz # Extract and set PATH tar -xzf graalvm-ce-java17-linux-amd64-22.3.1.tar.gz export GRAALVM_HOME=/path/to/graalvm export PATH=$GRAALVM_HOME/bin:$PATH # Install native-image gu install native-image
IDE Configuration
IntelliJ IDEA
<!-- Add to pom.xml for Maven --> <properties> <graalvm.home>/path/to/graalvm</graalvm.home> </properties>
VS Code
{
"java.home": "/path/to/graalvm",
"java.configuration.runtimes": [
{
"name": "JavaGraalVM",
"path": "/path/to/graalvm",
"default": true
}
]
}
3. Native Image Basics
Simple Native Image Example
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, Native World!");
System.out.println("Running on: " + System.getProperty("os.name"));
System.out.println("Java version: " + System.getProperty("java.version"));
}
}
Compilation Commands
# Compile with regular javac javac HelloWorld.java # Create native image native-image HelloWorld # Run native executable ./helloworld # With specific name native-image -o hello-app HelloWorld # Build with detailed output native-image --verbose HelloWorld
Build Configuration
# Common native-image options native-image \ --no-fallback \ --enable-https \ --enable-http \ --enable-url-protocols=http,https \ -H:Name=myapp \ -H:Class=com.myapp.Main \ -H:Path=./build \ -H:Log=registerResource \ --report-unsupported-elements-at-runtime \ --initialize-at-build-time=org.slf4j \ -jar myapp.jar
4. Building Native Spring Boot Applications
Maven Configuration
<?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>native-spring-app</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<graalvm.version>22.3.1</graalvm.version>
<spring-native.version>0.12.1</spring-native.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>${spring-native.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.19</version>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<mainClass>com.example.NativeSpringApplication</mainClass>
<buildArgs>
<buildArg>--enable-https</buildArg>
<buildArg>--enable-http</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
Spring Boot 3 Native Application
// NativeSpringApplication.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;
import java.time.Instant;
@SpringBootApplication
public class NativeSpringApplication {
public static void main(String[] args) {
SpringApplication.run(NativeSpringApplication.class, args);
}
}
@RestController
class HelloController {
private final UserService userService;
public HelloController(UserService userService) {
this.userService = userService;
}
@GetMapping("/")
public String hello() {
return "Hello from Native Spring Boot! Time: " + Instant.now();
}
@GetMapping("/users")
public List<User> getUsers() {
return userService.getAllUsers();
}
@GetMapping("/stats")
public RuntimeStats getStats() {
Runtime runtime = Runtime.getRuntime();
return new RuntimeStats(
runtime.availableProcessors(),
runtime.totalMemory(),
runtime.freeMemory(),
System.currentTimeMillis()
);
}
}
// UserService.java
package com.example;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
public List<User> getAllUsers() {
return List.of(
new User(1L, "John Doe", "[email protected]"),
new User(2L, "Jane Smith", "[email protected]"),
new User(3L, "Bob Johnson", "[email protected]")
);
}
}
// Records for data classes
record User(Long id, String name, String email) {}
record RuntimeStats(int processors, long totalMemory,
long freeMemory, long timestamp) {}
Application Configuration
# application.properties spring.application.name=native-spring-app server.port=8080 # H2 Database for native testing spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # JPA Configuration spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false # Native image specific spring.aop.proxy-target-class=false
5. Build Commands and Docker Integration
Maven Build Commands
# Regular JAR build mvn clean package # Build native image mvn -Pnative native:compile # Build with custom arguments mvn -Pnative native:compile -Dnative.buildArgs=--verbose # Build using Spring Boot mvn spring-boot:build-image # Run native executable ./target/native-spring-app
Docker Configuration
# Dockerfile for Native Image FROM ubuntu:22.04 as builder # Install necessary tools RUN apt-get update && apt-get install -y \ build-essential \ libz-dev \ zlib1g-dev \ && rm -rf /var/lib/apt/lists/* # Copy native executable COPY target/native-spring-app /app/native-spring-app # Create non-root user RUN useradd -m -u 1000 javauser USER javauser # Expose port EXPOSE 8080 # Run application ENTRYPOINT ["/app/native-spring-app"]
Multi-stage Docker Build
# Multi-stage Dockerfile FROM ghcr.io/graalvm/native-image:22.3.1 as builder WORKDIR /build # Copy source and build files COPY pom.xml . COPY src ./src # Build native image RUN mvn -Pnative native:compile # Runtime stage FROM gcr.io/distroless/base WORKDIR /app # Copy native executable COPY --from=builder /build/target/native-spring-app /app/ # Run as non-root USER 65534:65534 EXPOSE 8080 ENTRYPOINT ["/app/native-spring-app"]
Docker Compose
# docker-compose.yml version: '3.8' services: native-app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod restart: unless-stopped # Add other services like database postgres: image: postgres:15 environment: POSTGRES_DB: myapp POSTGRES_USER: appuser POSTGRES_PASSWORD: secret ports: - "5432:5432"
6. Reflection and Configuration
Reflection Configuration
// reflect-config.json
[
{
"name": "com.example.User",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "com.example.RuntimeStats",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
}
]
Native Image Properties File
# native-image.properties Args = --enable-https \ --enable-http \ -H:EnableURLProtocols=http,https \ -H:+ReportUnsupportedElementsAtRuntime \ -H:+ReportExceptionStackTraces \ --initialize-at-build-time=org.slf4j,ch.qos.logback \ --report-unsupported-elements-at-runtime
Programmatic Configuration
// ReflectionConfiguration.java
package com.example.config;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
import java.lang.reflect.Method;
import java.util.Arrays;
public class ReflectionConfiguration implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Register classes for reflection
registerForReflection(access, "com.example.User");
registerForReflection(access, "com.example.RuntimeStats");
registerForReflection(access, "com.example.HelloController");
// Register resource bundles
registerResourceBundles("messages", "validation");
}
private void registerForReflection(BeforeAnalysisAccess access, String className) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
RuntimeReflection.register(clazz.getDeclaredConstructors());
RuntimeReflection.register(clazz.getDeclaredMethods());
RuntimeReflection.register(clazz.getDeclaredFields());
} catch (ClassNotFoundException e) {
System.err.println("Class not found for reflection: " + className);
}
}
private void registerResourceBundles(String... baseNames) {
Arrays.stream(baseNames).forEach(baseName -> {
try {
RuntimeReflection.register(
java.util.ResourceBundle.getBundle(baseName).getClass()
);
} catch (Exception e) {
System.err.println("Resource bundle not found: " + baseName);
}
});
}
}
7. Advanced Native Image Features
Building with Gradle
// build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.1.0"
id("io.spring.dependency-management") version "1.1.0"
id("org.graalvm.buildtools.native") version "0.9.19"
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
}
graalvmNative {
binaries {
named("main") {
buildArgs.addAll(
"--enable-https",
"--enable-http",
"-H:EnableURLProtocols=http,https",
"-H:+ReportExceptionStackTraces",
"--initialize-at-build-time=org.slf4j"
)
}
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Custom Native Image Build
// Custom native image builder
package com.example;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class NativeImageBuilder {
public static void buildNativeImage(String mainClass, String outputName)
throws IOException, InterruptedException {
List<String> command = new ArrayList<>();
command.add("native-image");
command.add("--no-fallback");
command.add("--enable-https");
command.add("--enable-http");
command.add("-H:EnableURLProtocols=http,https");
command.add("-H:Name=" + outputName);
command.add("-H:Class=" + mainClass);
command.add("-H:Path=./target");
command.add("-H:+ReportExceptionStackTraces");
command.add("--report-unsupported-elements-at-runtime");
command.add("--initialize-at-build-time=org.slf4j,ch.qos.logback");
command.add("-cp");
command.add("target/classes:target/dependency/*");
command.add(mainClass);
ProcessBuilder pb = new ProcessBuilder(command);
pb.inheritIO();
System.out.println("Building native image...");
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("Native image built successfully: " + outputName);
} else {
throw new RuntimeException("Native image build failed with code: " + exitCode);
}
}
public static void main(String[] args) throws Exception {
buildNativeImage("com.example.NativeSpringApplication", "my-native-app");
}
}
8. Testing Native Applications
JUnit 5 Testing Configuration
// Base test class for native compatibility
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = NativeSpringApplication.class
)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
class NativeApplicationTests {
@Test
void contextLoads() {
// Basic context loading test
}
@Test
void helloEndpointWorks(TestRestTemplate restTemplate) {
ResponseEntity<String> response = restTemplate.getForEntity("/", String.class);
assertEquals(200, response.getStatusCodeValue());
assertTrue(response.getBody().contains("Hello from Native Spring Boot"));
}
@Test
void usersEndpointReturnsData(TestRestTemplate restTemplate) {
ResponseEntity<String> response = restTemplate.getForEntity("/users", String.class);
assertEquals(200, response.getStatusCodeValue());
assertNotNull(response.getBody());
}
}
Native Image Testing
# Test the native executable ./target/native-spring-app & APP_PID=$! # Wait for app to start sleep 5 # Test endpoints curl http://localhost:8080/ curl http://localhost:8080/users curl http://localhost:8080/stats # Kill the app kill $APP_PID
9. Performance Comparison
Benchmarking Class
package com.example;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
public class PerformanceBenchmark {
public static void main(String[] args) throws Exception {
System.out.println("=== Performance Benchmark ===");
long startTime = System.nanoTime();
long jvmStartTime = getJvmStartTime();
// Application startup and initial operation
performInitialization();
long endTime = System.nanoTime();
long totalTime = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
long jvmUptime = TimeUnit.NANOSECONDS.toMillis(endTime - jvmStartTime);
System.out.println("Total time: " + totalTime + " ms");
System.out.println("JVM uptime: " + jvmUptime + " ms");
System.out.println("Memory used: " +
(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024 + " MB");
// First request simulation
long firstRequestStart = System.nanoTime();
simulateFirstRequest();
long firstRequestTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstRequestStart);
System.out.println("First request time: " + firstRequestTime + " ms");
}
private static long getJvmStartTime() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
return TimeUnit.MILLISECONDS.toNanos(runtimeMXBean.getStartTime());
}
private static void performInitialization() {
// Simulate application initialization
try {
Thread.sleep(100); // Simulate startup work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void simulateFirstRequest() {
// Simulate first API request
try {
Thread.sleep(50); // Simulate request processing
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Performance Results Example
# JVM Version # Startup: 1.5 - 3 seconds # Memory: 100-200 MB # First Request: 100-500 ms # Native Image Version # Startup: 0.01 - 0.05 seconds # Memory: 20-50 MB # First Request: 1-10 ms
10. Common Issues and Solutions
Troubleshooting Common Problems
// Common native image issues and solutions
public class NativeImageTroubleshooting {
// Issue 1: Missing reflection configuration
public void reflectionIssue() {
// Solution: Add reflect-config.json or use @RegisterForReflection
}
// Issue 2: Resource not found
public void resourceIssue() {
// Solution: Register resources in resource-config.json
}
// Issue 3: Dynamic proxy generation
public void proxyIssue() {
// Solution: Add proxy-config.json or use -H:DynamicProxyConfigurationFiles
}
// Issue 4: Class initialization
public void classInitializationIssue() {
// Solution: Use --initialize-at-build-time or --initialize-at-run-time
}
}
// Annotation for easy reflection registration
import io.micronaut.core.annotation.TypeHint;
@TypeHint({
User.class,
RuntimeStats.class,
HelloController.class
})
class ReflectionConfigurationHint {}
Useful Native Image Options
# Debugging options --verbose -H:+ReportExceptionStackTraces -H:+TraceClassInitialization -H:+PrintClassInitialization # Memory options -Xmx4G # Build memory -Xms2G # Initial heap size # Performance options -O2 # Optimization level (0-2) --gc=G1 # Garbage collector # Feature options --enable-all-security-services --install-exit-handlers
11. Production Best Practices
Security Considerations
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable()) // For API applications
.build();
}
}
Monitoring and Health Checks
package com.example.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class NativeAppHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// Custom health checks
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
if (usedMemory > maxMemory * 0.9) {
return Health.down()
.withDetail("memory", "Critical memory usage")
.withDetail("used", usedMemory)
.withDetail("max", maxMemory)
.build();
}
return Health.up()
.withDetail("memory_used", usedMemory)
.withDetail("memory_max", maxMemory)
.withDetail("processors", runtime.availableProcessors())
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
Conclusion
When to Use GraalVM Native Image:
✅ Good Use Cases:
- Microservices and serverless functions
- CLI tools and utilities
- Resource-constrained environments
- Applications requiring fast startup
- Containerized deployments
❌ Less Suitable For:
- Applications with heavy reflection use
- Complex frameworks with dynamic classloading
- Applications requiring JVM tooling (JMX, JFR)
- Long-running applications where JIT optimization matters more
Key Benefits Summary:
- Startup Time: 10-100x faster
- Memory Usage: 5-10x lower
- Instant Performance: No warmup required
- Small Deployment Size: Single binary
- Better Security: Reduced attack surface
Migration Strategy:
- Start Simple: Begin with basic applications
- Incremental Migration: Convert services one by one
- Testing: Comprehensive testing of native builds
- Monitoring: Monitor performance in production
GraalVM Native Images represent the future of Java in cloud-native environments, offering unprecedented performance characteristics while maintaining Java's productivity benefits.