tl;dr: The tracing agent records behavior of a Java application running, for example, on GraalVM or any other compatible JVM, to provide the GraalVM Native Image Generator with configuration files for reflection, JNI, resource, and proxy usage. Enable it using java -agentlib:native-image-agent=...

Introduction

GraalVM Native Image Generation applies static analysis and ahead-of-time compilation to build a highly optimized native executable, called Native Image, for Java applications. This requires a closed-world assumption of reachable application classes: all classes need to be known at native image generation time so that the static analysis can process them.

The closed-world assumption conflicts with the open-world approach of Java reflection: Using functionality from the package java.lang.reflect , an application developer can look up classes, methods, and fields by name and access / invoke them. In typical cases, the names are loaded from configuration files or dynamically constructed at run time. Native image generation supports reflection, but requires all elements visible via reflection to be listed during image generation. The structure of the necessary .json file is explained in the GraalVM documentation.

In this article, we introduce a new tracing agent that produces .json files by observing the application behavior when running on the Java HotSpot VM, i.e., when running the application not as a native image. This leverages a common workflow: application development and testing is done using the Java HotSpot VM, and only the final application is then converted to a native image before deployment. During development and testing, the necessary reflection configuration files are built by tracing the Java HotSpot VM.

The tracing agent is part of the GraalVM download, both of the GraalVM Community Edition and the GraalVM Enterprise Edition. To enable it, use the option -agentlib:native-image-agent=... with the agent commands shown below.

Example

We use a minimal “Hello, world!” application that uses reflection as an example. For this blog post, we assume that the environment variable JAVA_HOME is set to a GraalVM installation directory (GraalVM 19 or later), and that the native-image tool has been installed.

Sample Java application using Reflection API.

The main method invokes all methods whose names are passed as command line arguments. Only two methods are provided for simplicity: foo and bar . Providing any other name on the command line leads to an exception that is printed.

Running the example via

$JAVA_HOME/bin/java HelloReflection foo xyz

produces the output

Running foo

Exception running xyz: NoSuchMethodException

As expected, the method foo was found via reflection, but the non-existent method xyz was not found.

As mentioned before, native image generation requires a configuration file, otherwise the method foo would not be accessible via reflection. To avoid confusion, the native image generator detects that reflection is used without a reflection configuration file: Running

$JAVA_HOME/bin/native-image HelloReflection

does not actually produce a native image of the application, but only a so-called “fallback image”:

...

Warning: Reflection method java.lang.Class.getMethod invoked at HelloReflection.main(HelloReflection.java:14)

Warning: Abort stand-alone image build due to reflection use without configuration.

Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception

...

Warning: Image 'helloreflection' is a fallback-image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).

The fallback image is just a launcher for the Java HotSpot VM. While this is probably not what the developer really wanted to produce, it is necessary to ensure that native image generation does not produce native images that fail immediately at run time, but perform as expected:

./helloreflection foo xyz

Running foo

Exception running xyz: NoSuchMethodException

We can explicitly disable the fallback image generation using the option --no-fallback :

$JAVA_HOME/bin/native-image --no-fallback HelloReflection

This produces a native image that can run without the Java HotSpot VM, but has no methods accessible via reflection:

./helloreflection foo xyz

Exception running foo: NoSuchMethodException

Exception running xyz: NoSuchMethodException

Reflection Tracing Agent

Writing a complete reflection configuration file from scratch is possible, but tedious. Therefore, we provide an agent for the Java HotSpot VM that produces a reflection configuration file by tracing all reflective lookup operations on the Java HotSpot VM. Operations that are traced are, for example, Class.forName , Class.getMethod , and Class.getField . The agent is part of the GraalVM download:

mkdir -p META-INF/native-image $JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image HelloReflection foo xyz

This command creates a directory META-INF/native-image with the file reflection-config.json . Several other files are created in that directory too, which we discuss later in this article. The file reflection-config.json makes the method HelloReflection.foo accessible via reflection:

[

{

"name":"HelloReflection",

"methods":[{ "name":"foo", "parameterTypes":[] }]

}

]

The native image generator automatically picks up configuration files in META-INF/native-image or subdirectories, the same way that native-image.properties files are automatically picked up.

$JAVA_HOME/bin/native-image HelloReflection

This produces a native image that allows reflective lookup of the method foo . Note that it is no longer necessary to provide the option --no-fallback : the reflection configuration file stated the intention of the developer that no fallback image should be generated despite of the fact that the application uses reflection. The native image runs as expected:

./helloreflection foo xyz

Running foo

Exception running xyz: NoSuchMethodException

And as expected the startup of the native image is instant:

Startup of the GraalVM native image of the sample application is near instant.

Completeness of Reflection Configuration

The tracing agent and the native image tool cannot automatically check that the traced reflection usage or the provided reflection configuration files are complete. In our example command lines, we have not provided the name of the method bar so far. This method is found when running our example on the Java HotSpot VM:

$JAVA_HOME/bin/java HelloReflection bar

Running bar

But it is not found when running the native image as generated in the previous section:

./helloreflection bar

Exception running bar: NoSuchMethodException

We either have to manually edit the file reflection-config.json and add the method bar , or we can run the tracing agent to augment the configuration file:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image HelloReflection bar

Note the different option config-merge-dir that instructs the agent to extend the existing configuration files instead of overwriting them with new configuration files. After re-building the native image, the method bar is now accessible too:

$JAVA_HOME/bin/native-image HelloReflection

...

./helloreflection foo bar xyz

Running foo

Running bar

Exception running xyz: NoSuchMethodException

For real-world applications, we suggest using both the tracing agent as well as manual inspection and modification of the configuration files. Running on the Java HotSpot VM on all test suites provided by an application can produce a fairly complete configuration file. The completeness depends on the code coverage of the test suite: An ideal test suite with 100% application code coverage produces a configuration file that is guaranteed to be complete. However, in reality test suites never test all paths through an application. Therefore, manual inspection and modification of the configuration files is likely to be required for real-world applications.

JNI, Resource, and Proxy Configuration

The native image generator requires configuration files not only for reflection, but also for several other functionality where the static analysis cannot automatically determine what to put into a native image:

JNI: Classes, methods, and fields that are accessed from C code via JNI must be registered using a file with the same structure as a reflection configuration file.

Resources: Application data files are often part of .jar files, alongside the classes. All resources that should be available at run time must be specified using a regular expression syntax, as documented here.

Proxy: The internal implementation of java.lang.reflect.Proxy generates classes for all combinations of interfaces passed to it. These combinations must be provided in a configuration file, as documented here.

Our agent traces the JNI, resource, and proxy usage of an application too, and produces the appropriate configuration files ( jni-config.json , resource-config.json , and proxy-config.json ).

Conclusions

The tracing agent observes the behavior of an application running on the Java HotSpot VM and writes configuration files to automatically configure the native image generator. We hope that it is a useful tool, both to get applications running as native images for the first time and as part of a continuous integration build/test system. The agent is a recent addition, so please let us know if you encounter bugs or if you are missing a particular feature.