Native Java Performance: GraalVM Native Image Compilation

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

  1. Reflection Configuration - Most common challenge, requires careful configuration
  2. Dynamic Features - Limited support for dynamic class loading and reflection
  3. Build Time - Longer compilation times compared to traditional builds
  4. 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.

Leave a Reply

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


Macro Nepal Helper