Tips and Strategies for Native Images in GraalVM

Tips and Strategies for Native Images in GraalVM

In the cloud computing ecosystem, efficiency and scalability are vital. GraalVM offers a robust solution by allowing the compilation of Java code into native executables, mitigating "cold start" time and enhancing performance.

GraalVM redefines the execution of Java, Kotlin, and Scala applications by facilitating Ahead-Of-Time (AOT) compilation, resulting in significant reductions in application size and startup time.

However, using GraalVM brings a series of challenges. In this article, I would like to show some of them that can help us have a more efficient native compilation.

Here the use of GraalVM is demonstrated along with the Micronaut framework.

Configurations, Problem Solving, and Debugging

Reducing Startup Time with GraalVM

The phenomenon known as "cold start" can negatively impact the scalability and response of systems in cloud environments, where readiness and efficiency are crucial. GraalVM directly addresses this challenge through Ahead-Of-Time (AOT) compilation, which transforms applications into native executables. This approach eliminates the runtime dependency of the JVM, resulting in significant improvements in startup time and resource efficiency, but adds more complexity as we have to make various configurations for a more effective compilation.

Crucial Tips for GraalVM Configuration

  • Avoid the JVM fallback feature if AOT compilation fails, using the --no-fallback parameter.

  • Do not defer compilation errors to runtime. Avoid parameters like --report-unsupported-elements-at-runtime.

  • Manage reflection issues by configuring GraalVM to recognize the necessary elements. This can be done by running comprehensive tests and using a special Java agent to track reflective calls.

The mentioned topics are related to Ahead-Of-Time (AOT) compilation and configuration of the Java Virtual Machine (JVM). Let's detail each of them:

Avoid JVM Fallback Feature if AOT Compilation Fails

Using the --no-fallback parameter is crucial to ensure that your application always runs as native code. AOT compilation pre-compiles Java code into native code before execution, in contrast to Just-in-Time (JIT) compilation that occurs at runtime. AOT compilation failure can lead some tools to automatically resort to JIT compilation, executing the code on the JVM as a fallback. Specifying --no-fallback prevents this behavior, helping to identify and fix compilation issues early, and ensuring that your application benefits from native code performance.

Do Not Defer Compilation Errors to Runtime

It's tempting to use parameters like --report-unsupported-elements-at-runtime to postpone the resolution of compatibility issues, allowing your application to compile, but this can introduce runtime errors. Tackling these issues during the compilation phase contributes to a more robust and reliable codebase, as well as improving performance by avoiding unwanted surprises in production.

In summary, these guidelines aim to ensure that AOT compilation is done robustly and reliably, detecting potential issues early and ensuring that code runs as intended, without resorting to the JVM or facing unexpected errors at runtime.

Speeding Up Development with quickBuild in GraalVM

GraalVM revolutionizes the way we build and run Java applications, offering the possibility to compile applications into native executables. One of the most powerful tools in this arsenal is quickBuild, a functionality designed to maximize efficiency during software development and testing. Here's how quickBuild can transform your workflow:

What is quickBuild?

quickBuild is a method or command in GraalVM that simplifies the compilation of native applications. It uses predefined or simplified settings to speed up the compilation process, allowing developers to focus on iterating and improving their applications quickly.

Benefits of Use

  • Rapid Development: Rapid iteration is essential in the software development lifecycle. quickBuild reduces waiting time, allowing for more agile product evolution.

  • Efficient Prototyping: Ideal for prototyping and experimentation, where speed in creating testable versions is prioritized over the optimization of the final product.

  • Acceleration in CI: In Continuous Integration (CI) environments, quickBuild can significantly speed up builds, contributing to smoother and more continuous delivery.

Important Considerations

Although quickBuild is a valuable tool for development and testing, it's important to note that the optimizations it sacrifices for speed may not be ideal for applications destined for production. Use it as a development and testing tool, and opt for more detailed and assertive configurations when you're ready to launch.

Addressing Dynamic Proxy Issues

  1. Dynamic Proxy Configuration: You need to explicitly list all the interfaces that will be used to create dynamic proxies. This is done through a JSON configuration file that must be provided to GraalVM.

  2. Generating Configuration File: To automatically generate this configuration file, you can use the GraalVM tracing agent while running your application in a test environment. The agent will capture the necessary information about dynamic proxies.

Handling Reflection Issues

  1. Reflection Configuration: Similar to dynamic proxies, GraalVM requires all classes, methods, and fields accessible via reflection to be declared in JSON configuration files.
    Reflection Use in Native Images

  2. Using the Tracing Agent for Reflection: Run your application with the GraalVM tracing agent enabled to automatically generate the required configuration files. This will include information about all reflection calls that occur during execution.
    Configure Native Image with the Tracing Agent

  3. Manual Configuration Maintenance: In some cases, it may be necessary to manually maintain the configuration files to ensure that all reflection calls are correctly recognized by GraalVM.
    Reflection in Native Image

    When it comes to specific reflection configurations in Micronaut with GraalVM, the mentioned annotations and commands are crucial:

    1. -H:ReflectionConfigurationFiles=src/graal/reflect-config.json:

      • This option is used to manually specify the reflection configuration files when building a native image with GraalVM.

      • You must generate or update the reflect-config.json file to include the classes and methods that need reflection access.
        Micronaut Framework- section 15.4 - Micronaut for GraalVM

    2. @ReflectionAccess (Micronaut):

      • This is a specific Micronaut annotation that can be used to indicate that a class or method should be included in the reflection configurations for native image compilation.

      • By annotating a class or method with @ReflectionAccess, you signal Micronaut to include these elements in the reflection configuration file.

        Micronaut Framework- section 15.4 - Micronaut for GraalVM

    3. @GenerateProxy (Micronaut):

      • This annotation is used to indicate that a proxy for a specific interface should be generated at compile time.

      • This is useful for creating proxies for interfaces without incurring reflection costs at runtime, making it compatible with GraalVM's native image compilation.

  • Examples of this approach can be observed in the Micronaut data API:
    Micronaut Data

These configurations and annotations are fundamental to optimizing the use of reflection and dynamic proxies in Micronaut, especially when working with native images in GraalVM, ensuring performance and compatibility. It's important to note that these configurations are abstracted by Micronaut, but they are concepts from GraalVM and are also applied in other frameworks like Quarkus with their own applicability nuances.

GraalVM Agents

GraalVM agents play a vital role in preparing applications for native image compilation. These agents, especially the tracing agent (Tracing Agent), are used to simplify the process of identifying and configuring the runtime dependencies needed for native image compilation.

  1. Tracing Agent: When run with a Java application, the tracing agent collects information about the use of reflection, resource access, dynamic proxies, and other APIs relevant to native compilation. It generates JSON configuration files that can be used during the native image compilation to ensure all dynamic behaviors are properly considered and included in the native image.

    Configure Native Image with the Tracing Agent

GraalVM Debug

Debugging in GraalVM, on the other hand, refers to tools and functionalities that allow debugging applications and the GraalVM runtime itself. This includes debugging traditional Java applications as well as native images compiled with GraalVM. Debugging tools are used to identify and resolve issues in the application, such as bugs, performance problems, memory leaks, among others.

Debug Native Images in VS Code

Using Gradle to Compile Native Images for Docker

Key Configuration Points:

  • Plugins: Start by adding the org.graalvm.buildtools.native plugin to your project. This enables the necessary functionalities to compile your application into a native executable.

  • Dependencies: Here you add the necessary dependencies for your project. This may include external libraries that your application needs.

  • Native Configuration: In the nativeBuild section, you configure the specific details for native compilation. This includes the name of the executable that will be generated (imageName). You can configure various options here, depending on the specific needs of your project.

Compiling the Project Using Gradle

To compile your project and generate a native executable using gradle, you would use the following command in the terminal:

Note: Gradle greatly facilitates the entire process of creating the native image.

gradlew nativeCompile

Gradle Plugin

Micronaut Guides | Micronaut Guides | Micronaut Framework

GraalVM Native Image Tips & Tricks
Reflectionless: Meet the New Trend in the Java World