Fix GraalVM Native Image Build Issues
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
andJAVA_HOME
environment variables are set to the GraalVM path. - The
$GRAALVM_HOME/bin
folder is included in thePATH
environment variable. - On Linux and macOS,
native-image
must be installed manually by executinggu 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.
- Rpm-based Linux:
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
- application.properties:
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:
- java.util.logging is fully supported
- Quarkus logging
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:
- 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>
- Execute all the Java app's possible end-to-end tests (http requests, etc.)
- Stop the Java app's process (CTRL-C)
- The native image config files will be generated in
<GENERATED_FILES_DIRECTORY>
- Move the generated files either to:
- a
META-INF/native-image
directory accessible from the classpath, for example within yoursrc/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/
- a
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 toreflect-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" } ] }
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.