GraalVM Native Image Quick Reference

Olga Gupalo
Feb 4, 2021 · 7 min read

We continue the series of short visual quick references about GraalVM. In this article, we highlight the most frequently used and important options and features of GraalVM Native Image.

Native Image allows compiling your applications to binaries, typically, the executable ones, but you can also create shared libraries. The executables are self-contained, start fast, very reasonable in size, all of which make them a perfect fit for container-based deployments both on-premises and cloud servers.

Below you see a one-page summary of the key options and commands for native-image.

GraalVM Native Image quick reference (web version, for printing please use pdf)

This reference fits neatly on an A4 paper, so you can hang it on a wall and use it as a reminder of what GraalVM can do and which options to use. Be sure to grab the pdf version for printing so it looks nice and sharp. Note that if your printer is more comfortable with the US letter paper formats please make sure to get this version, it’ll look better.

In this post, we’ll go over the information in the quick reference and describe it in more detail.

To start using native-image, you need GraalVM installed. Also make sure you install native-image as well, using the updater utility:

<graalvm-home>/bin/gu install native-image

For compilation, native-image depends on some local native libraries, which may differ from platform to platform. Meeting the system requirements will make using native images easier.

Building native images

With Native Image you can convert Java bytecode into a native executable or a native shared library. In both cases, an image should have at least one entry point method.

Native Executable

Building a standalone native executable is the default mode of operation. Entry point methods for the native image builder can be Java main methods with a signature that takes the command line arguments as an array of strings. The number of entry points is arbitrary.

public static void main(String[] arg) { /* ... */ }

Typically, you would build a native executable of your Java project from a JAR file with all dependencies.

native-image [options] -jar MyApp.jar [imagename]

You may also generate a native executable from a Java class file which is less practical unless you provide other packages, or JARs, or ZIPs on the classpath:

native-image [options] -cp jar:com/package/**/myApp.jar MyApp [imagename]

In the same way, you can specify any other Java class containing the main method to become the entry point for the native image builder: -H:Class=ClassName. The native executable file will be stored in the project’s target or user’s current working directory, but you can change the output directory, e.g., native-image path/imagename Class.

The native image builder will pick up any resources specified at build time. Use a regular expression to match the file names: -H:IncludeResources=com/package/**/fileName.xml. Similarly, you may pass the resources in bundles: -H:IncludeResourceBundles=<comma separated list of resources bundles>. A more detailed guide on accessing resources is available here.

Native Image supports class initialization at build time, but certain policies apply. You can pick what classes or packages get initialized at build or run time from the command line, using the --initialize-at-build-time and --initialize-at-run-time options, followed by a comma-separated list of packages and classes, and implicitly all their superclasses. An empty string designates all packages.

If you want to examine the building process closer, then you can attach a debugger: --debug-attach=[port], connect from your IDE and since the builder is a Java process, use your familiar tools to step through its execution.

The --enable-all-security-services option allows to include all security service classes into the image. To enable the HTTPS support, pass --enable-https (enabling all URL protocols is possible with --enable-url-protocols).

Other handy options are -H:+AddAllCharsets to add charsets support, and -H:+IncludeAllTimeZones to pre-initialize all timezones. This might increase the size of the resulting binary, but if you don’t know the exact combination of, for example, timezones your application will need, it’s a convenient solution.

The --install-exit-handlers option includes signal handlers and is preferable for building a native image for containerized environments, like Docker containers.

Another valuable option for container-based deployments is --static --libc=glibc|musl , which allows you to build a statically linked image without any additional library dependencies. You can also build a statically linked image with libc dynamically linked by passing the -H:+StaticExecutableWithDynamicLibC option. This is a common practice to use in combination with the distroless Docker images.

You can also build a “polyglot” native image by instructing the native image builder to load a specific language engine: --language:java|js|python|ruby|llvm|wasm .

Native Shared Library

The ability to build a native shared library opens yet another possibility to make your code useful in native applications. You can compile a Java API to a native shared library and then call it from another Java class file or even another language, like C or C++. For the inspiration why it could be interesting, check out this article on many ways of polyglot with GraalVM.

A static method that will serve as an entry point for your native shared library should be annotated with @CEntryPoint. Entry point methods must be static and may only have non-object parameters and return types. One of the parameters of an entry point method has to be of type IsolateThread or Isolate. There could be an arbitrary number of entry points. For example:

@CEntryPoint 
static int add(IsolateThread thread, int a, int b) {
return a + b;
}

To build a shared library of a JAR file, where you don’t specify the main class, specify the library name:

$ native-image --shared -jar jarfile [libraryname]

Native Image then generates the header file libraryname.h and the library, libraryname.so on Linux and libraryname.dylib on macOS. To load this library from another Java code, load or loadLibrary method will do: System.loadLibrary(“libraryname”); . To use the library from, for example, a C++ program, include the header file generated: #include <libraryname.h>. More examples and exhaustive documentation is available on the website.

Configuring the build

The native image builder performs static analysis to figure out all possible execution paths and writes that reachable data to an image. That’s why some dynamic Java features need configuration during the build process. For example, the builder should be informed about any reflective calls or resources to be loaded at run time in the form of configuration files or by passing a certain option at image build time.

To help you create configuration files, you can apply the Java agent provided with GraalVM. It will track all usages of dynamic features when executing your application on a JVM, and write them down to JSON files. If you tell the agent to put those files under the /META-INF/native-image/ location on the classpath, native-image will pick them up automatically. Run Java process with the tracing agent to generate the configuration:

$ java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ -jar MyApp.jar

Though you may need to instruct the native image builder to pick the configuration file(s) from a different location, specify the path:

$ native-image -H:ConfigurationFileDirectories=/path/to/config-dir/ -jar MyApp.jar

We recommend checking Native Image Build Configuration guide for more details.

Same way as running on a JVM, you can control the memory usage by the image at run time:

$ ./imagename -Xmx<m> -Xmn<m>

To configure default heap settings at build time, you can use the combination of -R:MaxHeapSize=<m> -R:MaxNewSize=<m> options to pre-set the config for the maximum heap size and the size for the young generation respectively.

Optimizing the executables

A native image is already a heavily optimized binary, but you can optimize it even more using profile-guided optimizations available with GraalVM Enterprise Edition:

$ native-image --pgo-instrument MyApp
$ ./myapp
$ native-image --pgo profile.iprof MyApp

For managing the Java heap at run time, Native Image provides different garbage collector (GC) implementations: the default one — Serial GC, and an adaptive and less prone to have longer GC pauses — G1 GC, which you can turn on with the --gc=G1 option.

Debugging

If you stumble upon a problem with your native image build process, it can be a number of things — misconfiguration, misunderstanding the class initialization strategies, the corner cases in supporting some of the more dynamic language features, and so on. Most probably it occurs in the dependencies you’re using or between your code and some library’s code. Sometimes, of course, it might possibly be the case that it is the native-image fault.

In any case, the error messages printed to the console usually suggest what to do. If it doesn’t provide any helpful information, even with verbose output enabled (--verbose ), there are several options to help you figure out the culprit:

  • Trace what classes or class objects were initialized: --trace-class-initialization
  • Trace the initialization path to a certain class: -H:+TraceClassInitialization=package.class.Name
  • Print classes intialized detected by the static analysis: -H:+PrintClassInitialization
  • Print the garbage collection logs: ./imagename -XX:+PrintGC -XX:+VerboseGC
  • Rebuild a native image with debugging symbols and trying to debug it using gdb: -g
  • If anything else fails, please submit an issue to the GitHub repository or contact us on the GraalVM community slack

If you suspect that your native executable file size is too large, or you wonder what packages, classes, and methods got packed into it, gather the diagnostic data into a dump file at image build time, and later visualize it with the GraalVM Dashboard tool:

$ native-image -H:+DashboardAll -H:DashboardDump=<path> -jar MyApp.jar
Code Size Breakdown window in GraalVM Dashboard

Conclusion

In this quick reference, we tried to outline the most frequently used options for GraalVM native-image utility.

You can download and print it out. Hopefully, it’ll keep reminding you about frequently used and the most useful options for the Native Image technology.

And while you’re at it, get GraalVM and try building a native image of your code — the startup times and low memory usage can really make a difference when running in constrained environments like containers or small cloud instances!

graalvm

GraalVM team blog - https://www.graalvm.org