Introduction
GraalVM Native Image is an innovative technology that compiles Java applications ahead-of-time (AOT) into native executables. These native binaries start instantly, have minimal memory footprint, and deliver peak performance without the need for a Java Virtual Machine (JVM) at runtime.
What is GraalVM Native Image?
Key Concepts
- Ahead-of-Time (AOT) Compilation - Compiles Java bytecode to native machine code during build time
- Substrate VM - A lightweight runtime that replaces the JVM
- Closed-World Analysis - Analyzes all reachable code at build time
- Native Executables - Standalone binaries that don't require JRE
Benefits
- Instant Startup - Milliseconds instead of seconds
- Reduced Memory Footprint - Typically 1/10th of JVM memory usage
- Lower CPU Overhead - No JIT compilation at runtime
- Small Deployment Size - Single executable, no JRE dependency
- Container Optimization - Perfect for microservices and serverless
Setup and Installation
1. Install GraalVM
# Download GraalVM from https://www.graalvm.org/downloads/ # Extract and set environment variables # For Linux/Mac export GRAALVM_HOME=/path/to/graalvm export PATH=$GRAALVM_HOME/bin:$PATH # For Windows set GRAALVM_HOME=C:\path\to\graalvm set PATH=%GRAALVM_HOME%\bin;%PATH% # Verify installation java -version # Should show GraalVM JDK
2. Install Native Image
# Using GraalVM Updater gu install native-image # Or download manually gu install -L https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.0/native-image-installable-svm-java17-linux-amd64-22.3.0.jar
3. Maven Dependencies
<properties>
<graalvm.version>22.3.0</graalvm.version>
<spring-boot.version>3.0.0</spring-boot.version>
<native-build-tools.version>0.9.18</native-build-tools.version>
</properties>
<dependencies>
<!-- Spring Boot Native Support -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.12.1</version>
</dependency>
<!-- For Spring Boot 3+ -->
<dependency>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-build-tools.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-build-tools.version}</version>
<extensions>true</extensions>
<configuration>
<mainClass>com.example.Application</mainClass>
<buildArgs>
<buildArg>--verbose</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
<!-- Spring Boot Maven 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>
Basic Native Image Compilation
1. Simple Java Application
package com.example;
public class HelloNative {
public static void main(String[] args) {
System.out.println("Hello from Native Image!");
System.out.println("Arguments: " + String.join(", ", args));
}
}
2. Compilation Commands
# Compile to class files javac -d target/classes src/main/java/com/example/HelloNative.java # Create native image native-image -cp target/classes com.example.HelloNative hello-native # Run native executable ./hello-native "test argument" # With more options native-image \ --no-fallback \ --enable-http \ --enable-https \ -H:Name=myapp \ -cp target/classes \ com.example.HelloNative
Spring Boot Native Applications
1. Spring Boot 3 Application
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 NativeApplication {
public static void main(String[] args) {
SpringApplication.run(NativeApplication.class, args);
}
}
@RestController
class HelloController {
@GetMapping("/")
public String hello() {
return "Hello from Native Spring Boot!";
}
@GetMapping("/info")
public RuntimeInfo info() {
return new RuntimeInfo(
Runtime.version().toString(),
Runtime.getRuntime().availableProcessors(),
System.getProperty("java.vm.name")
);
}
record RuntimeInfo(String version, int processors, String vmName) {}
}
2. Application Configuration
# application.yml
spring:
application:
name: native-demo
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
server:
port: 8080
logging:
level:
com.example: DEBUG
3. Native-Specific Configuration
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
@Configuration
public class NativeConfig {
// This configuration helps GraalVM with reflection at build time
@Bean
public Feature nativeFeature() {
return new Feature() {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Register classes for reflection
registerForReflection("com.example.model.User");
registerForReflection("com.example.model.Role");
}
private void registerForReflection(String className) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
RuntimeReflection.register(clazz.getDeclaredFields());
RuntimeReflection.register(clazz.getDeclaredMethods());
RuntimeReflection.register(clazz.getDeclaredConstructors());
} catch (ClassNotFoundException e) {
// Handle exception
}
}
};
}
}
Reflection Configuration
1. JSON Reflection Configuration
// src/main/resources/META-INF/native-image/reflect-config.json
[
{
"name": "com.example.model.User",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "com.example.model.Role",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "com.example.dto.ApiResponse",
"methods": [
{
"name": "<init>",
"parameterTypes": ["java.lang.Object", "java.lang.String"]
}
]
}
]
2. Resource Configuration
// src/main/resources/META-INF/native-image/resource-config.json
{
"resources": {
"includes": [
{
"pattern": "application.yml"
},
{
"pattern": "logback-spring.xml"
},
{
"pattern": "db/migration/.*\\.sql$"
},
{
"pattern": "templates/.*\\.html$"
},
{
"pattern": "META-INF/spring.factories"
},
{
"pattern": "META-INF/services/.*"
}
],
"excludes": [
{
"pattern": ".*\\.gitkeep"
}
]
},
"bundles": [
{
"name": "com.sun.org.apache.xerces.internal.impl.msg.XMLMessages"
}
]
}
3. Serialization Configuration
// src/main/resources/META-INF/native-image/serialization-config.json
[
{
"name": "com.example.model.User"
},
{
"name": "com.example.model.Role"
},
{
"name": "java.util.ArrayList"
},
{
"name": "java.util.HashMap"
}
]
Build Configuration
1. Maven Profile for Native Build
<profiles>
<profile>
<id>native</id>
<properties>
<native.build>true</native.build>
</properties>
<dependencies>
<dependency>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-build-tools.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-build-tools.version}</version>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<mainClass>com.example.NativeApplication</mainClass>
<imageName>${project.artifactId}</imageName>
<buildArgs>
<buildArg>--verbose</buildArg>
<buildArg>-H:EnableURLProtocols=http,https</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
<buildArg>-H:+ReportUnsupportedElementsAtRuntime</buildArg>
<buildArg>-H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image</buildArg>
<buildArg>--initialize-at-build-time=org.slf4j.MDC</buildArg>
<buildArg>--initialize-at-run-time=com.example.dynamic.DynamicService</buildArg>
<buildArg>-H:IncludeResources=application.yml</buildArg>
<buildArg>-H:Log=registerResource</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
2. Gradle Configuration
plugins {
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
id 'org.graalvm.buildtools.native' version '0.9.18'
}
graalvmNative {
binaries {
main {
imageName = 'my-native-app'
mainClass = 'com.example.NativeApplication'
buildArgs.addAll(
'--verbose',
'--no-fallback',
'-H:EnableURLProtocols=http,https',
'-H:+ReportExceptionStackTraces',
'-H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image'
)
resources.autodetect()
}
}
toolchainDetection = false
}
tasks.named('test') {
useJUnitPlatform()
}
Advanced Configuration
1. Dynamic Proxy Configuration
// src/main/resources/META-INF/native-image/proxy-config.json
[
{
"interfaces": [
"org.springframework.data.repository.Repository",
"com.example.repository.UserRepository"
]
},
{
"interfaces": [
"org.springframework.transaction.interceptor.TransactionInterceptor"
]
},
{
"interfaces": [
"org.springframework.aop.SpringProxy",
"org.springframework.aop.framework.Advised",
"org.springframework.core.DecoratingProxy"
]
}
]
2. JNI Configuration
// src/main/resources/META-INF/native-image/jni-config.json
[
{
"name": "java.lang.ClassLoader",
"methods": [
{
"name": "loadClass",
"parameterTypes": ["java.lang.String"]
}
]
}
]
Database Integration
1. JPA Entity Configuration
package com.example.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(unique = true, nullable = false)
private String email;
private String firstName;
private String lastName;
@Enumerated(EnumType.STRING)
private UserStatus status;
private LocalDateTime createdAt;
// Constructors, getters, setters
public User() {}
public User(String username, String email, String firstName, String lastName) {
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.status = UserStatus.ACTIVE;
this.createdAt = LocalDateTime.now();
}
public enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}
// Getters and setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
2. Spring Data JPA Repository
package com.example.repository;
import com.example.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByStatus(User.UserStatus status);
@Query("SELECT u FROM User u WHERE u.firstName LIKE %:name% OR u.lastName LIKE %:name%")
List<User> findByNameContaining(@Param("name") String name);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
3. Native Image Configuration for JPA
// Additional reflection entries for JPA
[
{
"name": "com.example.model.User",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "com.example.model.User$UserStatus",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "org.hibernate.dialect.PostgreSQLDialect",
"allDeclaredConstructors": true,
"allPublicConstructors": true
}
]
Testing Native Images
1. Native Image Test Configuration
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class NativeApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
// Basic context loading test
}
@Test
void helloEndpoint() {
ResponseEntity<String> response = restTemplate.getForEntity("/", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("Hello from Native Spring Boot");
}
@Test
void infoEndpoint() {
ResponseEntity<String> response = restTemplate.getForEntity("/info", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("version");
}
}
2. Native Test Profile
# application-test.yml spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: sa password: driver-class-name: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true sql: init: data-locations: classpath:test-data.sql logging: level: com.example: DEBUG org.hibernate.SQL: DEBUG
Performance Optimization
1. Build-Time Initialization
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.Feature;
@Configuration
public class OptimizationConfig {
@Bean
public Feature buildTimeInitialization() {
return new Feature() {
@Override
public void duringSetup(DuringSetupAccess access) {
// Initialize at build time for better startup
System.setProperty("spring.native.mode", "native");
}
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Pre-initialize heavy components
access.registerSubtypeReachabilityHandler(
(duringAccess, subtype) -> {
// Force initialization of subtypes
},
access.findClassByName("org.springframework.context.ConfigurableApplicationContext")
);
}
};
}
}
2. Native Image Build Arguments
native-image \ --no-fallback \ --initialize-at-build-time= \ org.springframework,\ org.hibernate,\ com.fasterxml.jackson,\ org.slf4j \ --initialize-at-run-time= \ com.example.dynamic,\ sun.nio.ch,\ sun.security.ssl \ -H:+ReportExceptionStackTraces \ -H:+PrintClassInitialization \ -H:Name=optimized-app \ -cp app.jar \ com.example.Application
Docker Integration
1. Dockerfile for Native Image
# Multi-stage build for native image FROM ghcr.io/graalvm/native-image:22.3.0 AS native-build # Install build dependencies RUN microdnf install -y gcc glibc-devel zlib-devel # Set working directory WORKDIR /build # Copy project files COPY . . # Build native image RUN ./mvnw -Pnative native:compile -DskipTests # Runtime stage FROM alpine:3.17 # Install runtime dependencies RUN apk add --no-cache libstdc++ # Create non-root user RUN addgroup -S app && adduser -S app -G app # Copy native executable COPY --from=native-build /build/target/my-native-app /app/ # Switch to non-root user USER app # Expose port EXPOSE 8080 # Run application CMD ["/app/my-native-app"]
2. Docker Compose for Development
version: '3.8' services: app: build: context: . dockerfile: Dockerfile ports: - "8080:8080" environment: - DB_URL=jdbc:postgresql://db:5432/mydb - DB_USERNAME=app_user - DB_PASSWORD=app_pass depends_on: - db db: image: postgres:15 environment: - POSTGRES_DB=mydb - POSTGRES_USER=app_user - POSTGRES_PASSWORD=app_pass ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data:
Troubleshooting Common Issues
1. Reflection Errors
package com.example.util;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
@Configuration
@ImportRuntimeHints(ReflectionConfig.ReflectionHints.class)
public class ReflectionConfig {
static class ReflectionHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register for reflection
hints.reflection().registerType(
com.example.model.User.class,
hint -> hint.withMembers(
org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_METHODS,
org.springframework.aot.hint.MemberCategory.DECLARED_FIELDS
)
);
// Register resources
hints.resources().registerPattern("application.yml");
hints.resources().registerPattern("db/migration/*.sql");
}
}
}
2. Dynamic Proxy Issues
package com.example.config;
import org.springframework.aot.hint.ProxyHints;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
@Configuration
@ImportRuntimeHints(ProxyConfig.ProxyHintsConfig.class)
public class ProxyConfig {
static class ProxyHintsConfig implements RuntimeHintsRegistrar {
@Override
public void registerHints(org.springframework.aot.hint.RuntimeHints hints,
ClassLoader classLoader) {
ProxyHints proxyHints = hints.proxies();
// Register Spring Data repositories
proxyHints.registerJdkProxy(
org.springframework.data.repository.Repository.class,
org.springframework.transaction.interceptor.TransactionInterceptor.class,
org.springframework.aop.SpringProxy.class
);
// Register service interfaces
proxyHints.registerJdkProxy(
com.example.service.UserService.class
);
}
}
}
Benchmarking and Monitoring
1. Startup Time Measurement
package com.example.monitoring;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class StartupMonitor {
private final long startTime = System.currentTimeMillis();
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
long startupTime = System.currentTimeMillis() - startTime;
System.out.printf("Application started in %d ms%n", startupTime);
// Log memory usage
Runtime runtime = Runtime.getRuntime();
long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
long maxMemory = runtime.maxMemory() / (1024 * 1024);
System.out.printf("Memory usage: %d MB / %d MB%n", usedMemory, maxMemory);
}
}
2. Performance Comparison
# JVM Mode time java -jar myapp.jar # Native Mode time ./myapp # Memory comparison ps -o pid,rss,command -p $(pgrep -f myapp)
Best Practices
1. Development Workflow
# 1. Develop and test in JVM mode ./mvnw spring-boot:run # 2. Build native image for testing ./mvnw -Pnative native:compile # 3. Test native executable ./target/myapp # 4. Profile and optimize ./mvnw -Pnative native:compile -Dnative.buildArgs="-H:+AllowIncompleteClasspath"
2. Configuration Management
package com.example.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
public class NativeEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
if (isNativeImage()) {
environment.getPropertySources().addFirst(
new MapPropertySource("native-properties", Map.of(
"spring.jpa.show-sql", "false",
"spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults", "false",
"logging.level.org.hibernate", "WARN"
))
);
}
}
private boolean isNativeImage() {
return System.getProperty("org.graalvm.nativeimage.imagecode") != null;
}
}
Conclusion
GraalVM Native Image offers significant benefits for Java applications:
- Instant startup times - milliseconds instead of seconds
- Reduced memory footprint - typically 1/10th of JVM usage
- Lower CPU overhead - no JIT compilation at runtime
- Small deployment size - single executable
- Container optimization - perfect for cloud-native deployments
Key Considerations
- Reflection Configuration - Most common challenge, requires careful configuration
- Dynamic Features - Limited support for dynamic class loading and reflection
- Build Time - Longer compilation times compared to traditional builds
- Debugging - Different debugging experience than JVM applications
When to Use Native Image
- ✅ Microservices - Fast startup and low memory
- ✅ Serverless Functions - Cold start optimization
- ✅ CLI Tools - Instant execution
- ✅ Resource-Constrained Environments - Limited memory/CPU
- ❌ Applications with heavy reflection - Complex configuration needed
- ❌ Applications requiring dynamic class loading - Limited support
GraalVM Native Image represents the future of Java in cloud-native environments, offering unprecedented performance characteristics while maintaining Java's productivity and ecosystem benefits.