GraalVM and Native Images in Java: Complete Guide to Native Java Applications

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:

  1. Start Simple: Begin with basic applications
  2. Incremental Migration: Convert services one by one
  3. Testing: Comprehensive testing of native builds
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper