Fix GraalVM Native Image Build Issues


Debug GraalVM native image builds
Understand and troubleshoot your GraalVM native image compilation and runtime errors and solve them with custom or agent-generated configuration, or with code substitutions.

Introduction

After discovering the GraalVM Native Image tool, you are now trying to compile ahead-of-time your Java / Kotlin / JVM app to a standalone binary executable.

However, you are facing a blocking issue during the app's build phase or at runtime.

This tutorial will help you troubleshoot this problem and fix your errors to profit from a faster startup time and a lower runtime memory overhead.

Getting started

If you are building inside a Docker image made specifically for GraalVM native image such as oracle/graalvm-ce or quay.io/quarkus/ubi-quarkus-native-image, you can skip this section.

GraalVM can be downloaded from this page.

Make sure your build environment is correctly configured:

  • The GRAALVM_HOME and JAVA_HOME environment variables are set to the GraalVM path.
  • The $GRAALVM_HOME/bin folder is included in the PATH environment variable.
  • On Linux and macOS, native-image must be installed manually by executing gu install native-image.
  • GCC and the glibc-devel & zlib-devel headers are required to be installed on the system:
    • Rpm-based Linux: sudo yum install gcc glibc-devel zlib-devel libstdc++-static
    • Debian-based Linux: sudo apt-get install build-essential libz-dev zlib1g-dev
    • macOS with XCode: xcode-select --install
    • Windows: Even though it's possible, I would not recommend using Windows directly to build native images. If you must, you can follow the instructions on this guide to install MSVC 2017 and initialize it before each build. You can alternatively use the Windows Subsystem for Linux or build using Docker.

Native-image command-line menu
Native-image command-line menu

Build-time errors

One of the first things to add is the --allow-incomplete-classpath build argument to avoid errors such as com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: xxx caused by missing classes during compile-time.

Additionally, if you will be going to use https to expose or consume APIs, add the arguments:

  • -H:EnableURLProtocols=http,https
  • --enable-all-security-services

When using a properties file, a comma can be escaped using \\. For example:

quarkus.native.additional-build-args=-H:EnableURLProtocols=http\\,https,--enable-all-security-services

Out of memory

One of the most common problems when building native images are out of memory errors:

  • Error: Image build request failed with exit status 1
  • Error: Image build request failed with exit status 134
  • Error: Image build request failed with exit status 137

To avoid them, explicitly set the maximum heap size used during the build to at least 4 GB (or more for more complex apps):

  • As an argument: native-image -J-Xmx4g
  • With Quarkus:
    • application.properties: quarkus.native.additional-build-args=-J-Xmx4g,...
    • Maven or Gradle build argument: -Dquarkus.native.native-image-xmx=4g

File descriptor in the image heap

GraalVM doesn't allow file descriptors to be referenced in static fields because the files might not be present at run time:

Error: Detected a FileDescriptor in the image heap

This is particularly problematic when using file appenders / handlers with Log4j / Logback or other logging solutions.

If the error is not relating to a logging framework, you can try to initialize the class containing the static field at runtime with the --initialize-at-run-time build argument, for example: native-image --initialize-at-run-time=com.example.ClassName,org.package.only,....

However, if the error is relating to a logging framework such as Log4j and Logback, there are currently no workarounds without using an alternative logging solution:


Missing type during build-time error
Missing type during build-time error
Out of memory error
Out of memory error
File descriptor in the image heap error
File descriptor in the image heap error

Runtime errors

After fixing the build-time errors, you may experience some unusual errors when you launch the native application. These errors are mainly relating to the GraalVM limitations, and thus require special configuration. Error examples include:

  • java.lang.IllegalArgumentException: Class xxx is instantiated reflectively but was never registered
  • java.lang.InstantiationException: Type xxx can not be instantiated reflectively as it does not have a no-parameter constructor or the no-parameter constructor has not been added explicitly to the native image
  • java.lang.ClassNotFoundException: xxx
  • java.lang.IllegalStateException: input must not be null

GraalVM does provide a way to generate configuration files for native image builds by running you're JVM app with an agent:

  1. Run the app's jar with the native image agent:
    <GRAALVM_HOME>/bin/java -agentlib:native-image-agent=config-output-dir=<GENERATED_FILES_DIRECTORY> -jar <JAR_FILE>
  2. Execute all the Java app's possible end-to-end tests (http requests, etc.)
  3. Stop the Java app's process (CTRL-C)
  4. The native image config files will be generated in <GENERATED_FILES_DIRECTORY>
    Native image configuration files
  5. Move the generated files either to:
    • a META-INF/native-image directory accessible from the classpath, for example within your src/main/resources directory
    • a system directory and specify it with the build argument: -H:ConfigurationFileDirectories=/path/to/config-dir/ or
    • a classpath directory and specify it with the build argument: -H:ConfigurationResourceRoots=path/to/resources/

You can run this procedure multiple times and merge the generated configuration files with the native-image-configure tool:

native-image-configure generate --input-dir=/path/to/config-dir-0/ --input-dir=/path/to/config-dir-1/ --output-dir=/path/to/merged-config-dir/

Tips

  • To fix class not found errors, add the class name to reflect-config.json:
    {
      //...
      {
        "name": "java.util.List"
      }
    }
  • The service provider (located in META-INF/services) of the used libraries have to be added to reflect-config.json:
    {
      //...
      {
        "name": "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule",
        "allDeclaredMethods": true,
        "allDeclaredConstructors": true
      }
    }
  • To discover service provider resources, add the following to resource-config.json:
    {
      "resources": [
        //...
        {
          "pattern": "META-INF/services/.*"
        }
      ]
    }
  • For Kotlin support, , add the following to resource-config.json:
    {
      "resources": [
        //...
        {
          "pattern": "META-INF/.*.kotlin_module$"
        },
        {
          "pattern": ".*.kotlin_builtins"
        }
      ]
    }

Runtime error example
Runtime error example
Native image agent
Native image agent

Advanced techniques

Sometimes, you are not able to fix the errors even when using the techniques discussed in the previous sections.

If you can change the code in question, try lazy initialization (will not work with unsupported features such as java.lang.invoke methods):

  • Extract the initialization code to a method, call the initialization method when needed. For thread safety, use double-checked locking.
  • If you're using Kotlin, you can use the lazy function.

For third-party libraries, there is a way to tell GraalVM how to alter any source code so that it can become compatible with native image builds: Substitutions.

Substitutions

Substitutions are usually required for unsupported GraalVM features. An error example when using such features:

Error: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported type java.lang.invoke.MemberName is reachable: All methods from java.lang.invoke should have been replaced during image building.

In order to implement your own substitutions, you must add the org.graalvm.nativeimage:svm dependency (provided / compileOnly) to your project (use the same version as the target GraalVM).

Basically a class annotated with @TargetClass is used to replace and delete the methods and fields of the original class using the @Substitute and @Delete annotations. The @Alias annotation is used to get a reference to the original fields, methods and constructors without using reflection. Here is an example:

import com.oracle.svm.core.SubstrateUtil;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.core.annotate.TargetElement;
import com.oracle.svm.core.jdk.JDK8OrEarlier;

import java.net.URL;

@TargetClass(className = "java.lang.Package")
final class Target_java_lang_Package {

  @Alias
  @SuppressWarnings({"unused"})
  Target_java_lang_Package(String name,
                           String spectitle, String specversion, String specvendor,
                           String impltitle, String implversion, String implvendor,
                           URL sealbase, ClassLoader loader) {
  }

  @Substitute
  @TargetElement(onlyWith = JDK8OrEarlier.class) // Substitute only when Java version <= 8
  static Package getPackage(Class<?> c) {
    if (c.isPrimitive() || c.isArray()) {
      /* Arrays and primitives don't have a package. */
      return null;
    }

    /* Logic copied from java.lang.Package.getPackage(java.lang.Class). */
    String name = c.getName();
    int i = name.lastIndexOf('.');
    if (i != -1) {
      name = name.substring(0, i);
      Target_java_lang_Package pkg = new Target_java_lang_Package(name, null, null, null,
        null, null, null, null, null);
      return SubstrateUtil.cast(pkg, Package.class); // Cast between the TargetClass and the original class
    } else {
      return null;
    }
  }
}

You can browse more substitutions examples in the com.oracle.svm.core.jdk package to get inspired, for example JavaLangSubstitutions.java.


Soufiane Sakhi is an AWS Certified Solutions Architect – Associate and a professional full stack developer.