Working with Random/SplittableRandom instances in GraalVM and Mandrel native images

Introduction

Embedding instances of Random and SplittableRandom in native images most often results in undesirable effects. These classes are meant to provide random values and are typically expected to get a fresh seed in each run. Embedding them in a native image results in the seed value that was generated at build-time to be cached in the native image, thus breaking that expectation.

NOTE:

An instance of a class A gets embedded in a native image when a reference to it is also embedded in the native image. This may happen because an instance of another class B has a field (static or not) of type A, or of a subtype of A.

As a result, in order to prevent developers from accidentally embedding instances of Random and SplittableRandom in native images as of GraalVM / Mandrel 21.1.0 an error is printed whenever such an instance is to get embedded in a native image. The error looks like this:

Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected. To see how this object got instantiated use –trace-object-instantiation=java.security.SecureRandom. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option –initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.

The aim of this tutorial is to provide some guideline on how to handle this kind of errors.

Prerequisites

This tutorial assumes that you have Mandrel or GraalVM 22.0 installed on your system. If you don't, you can install it using sdkman:

  sdk install java 22.0.0.2.r17-mandrel

or grab the release from GitHub.

How to handle such cases

Usually, running the native image compilation with --trace-object-instantiation=my.package.A (as instructed by the initial error message) provides enough info about the case at hand. To demonstrate this we will create a demo application and go through all the steps from building it to tracing the object instantiation and fixing the issue.

Native compilation of simple hello world

First let's create a simple hello world app by copy pasting the following code in a Main.java file.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Compiling the above example in a native image is as simple as:

sdk use java 22.0.0.2.r17-mandrel
javac Main.java
native-image Main

and then we can run it with ./main.

Native compilation of simple hello world using Random

Now let's make it use a static field of type Random and see what happens.

We expand our demo application to:

import java.util.Random;

public class Main {
    static Random random = new Random();

    public static void main(String[] args) {
        System.out.println("Hello world " + random.nextInt(100) + " !");
    }
}

to make it print a random number in the range [0-100) each time it's invoked.

Testing it with the JVM:

javac Main.java
for i in {0..10} java Main

results in something like:

Hello world 30 !
Hello world 12 !
Hello world 45 !
Hello world 97 !
Hello world 87 !
Hello world 14 !
Hello world 38 !
Hello world 75 !
Hello world 68 !
Hello world 19 !
Hello world 43 !

Similarly, testing it with native image:

native-image Main
for i in {0..10} ./main

results in something like:

Hello world 35 !
Hello world 9 !
Hello world 3 !
Hello world 53 !
Hello world 44 !
Hello world 14 !
Hello world 45 !
Hello world 50 !
Hello world 57 !
Hello world 29 !
Hello world 25 !

But we said earlier that native-image doesn't allow instances of Random in the native-image and that even if it did the seed would be cached resulting in non random results so why is this working?

The reason our example compiles and runs without issue is that the Main class is not being initialized at build-time. That means that Main is also not stored in the native image heap and as a result no instances of Random end up in the native image heap as well.

To demonstrate this let's add a static initializer to Main.

import java.util.Random;

public class Main {
    static Random random = new Random();

    static {
        System.out.println("Hello from the static initializer!");
    }

    public static void main(String[] args) {
        System.out.println("Hello world " + random.nextInt(100) + " !");
    }
}

recompiling it to a native image and running it:

javac Main.java
native-image Main
./main

we see that the static initializer is being run at run-time and not at build-time, which confirms that Main is not being build-time initialized and stored in the native image heap:

Hello from the static initializer!
Hello world 18 !

Forcing a class to be initialized at build-time

For the needs of this tutorial we are going to instruct native-image to initialize Main at build-time using the parameter initialize-at-build-time=Main:

native-image --initialize-at-build-time=Main Main

which will result in the following output:

========================================================================================================================
GraalVM Native Image: Generating 'main'...
========================================================================================================================
[1/7] Initializing...                                                                                    (2.6s @ 0.12GB)
Hello from the static initializer!
 Version info: 'GraalVM 22.0.0.2-Final Java 17 Mandrel Distribution'
[2/7] Performing analysis...  [*******]                                                                  (9.6s @ 0.45GB)
   2,729 (83.92%) of  3,252 classes reachable
   3,333 (60.27%) of  5,530 fields reachable
  12,343 (73.22%) of 16,857 methods reachable
      33 classes,     0 fields, and   178 methods registered for reflection
      57 classes,    58 fields, and    51 methods registered for JNI access

Warning: Aborting stand-alone image build due to unsupported features
Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
------------------------------------------------------------------------------------------------------------------------
                        0.5s (3.5% of total time) in 12 GCs | Peak RSS: 1.60GB | CPU load: 6.35
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /home/foivos.zakkak.net/content/tutorials/main.build_artifacts.txt
========================================================================================================================
Failed generating 'main' after 12.5s.
========================================================================================================================
GraalVM Native Image: Generating 'main'...
========================================================================================================================
[1/7] Initializing...                                                                                    (3.0s @ 0.13GB)
 Version info: 'GraalVM 22.0.0.2-Final Java 17 Mandrel Distribution'
[2/7] Performing analysis...  [*******]                                                                 (12.9s @ 0.48GB)
   2,770 (84.17%) of  3,291 classes reachable
   3,355 (60.18%) of  5,575 fields reachable
  12,484 (73.38%) of 17,012 methods reachable
      33 classes,     0 fields, and   178 methods registered for reflection
      57 classes,    58 fields, and    51 methods registered for JNI access
[3/7] Building universe...                                                                               (0.9s @ 0.65GB)
[4/7] Parsing methods...      [*]                                                                        (0.9s @ 0.88GB)
[5/7] Inlining methods...     [****]                                                                     (1.4s @ 0.79GB)
[6/7] Compiling methods...    [***]                                                                     (10.4s @ 0.93GB)
[7/7] Creating image...                                                                                  (1.8s @ 1.24GB)
   4.03MB (33.53%) for code area:    7,475 compilation units
   6.93MB (57.62%) for image heap:   1,668 classes and 91,492 objects
   1.06MB ( 8.85%) for other data
  12.02MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
 635.61KB java.util                                            1.77MB byte[] for general heap data
 312.92KB java.lang                                          796.69KB java.lang.String
 283.85KB java.text                                          596.90KB java.lang.Class
 233.44KB java.util.regex                                    490.33KB byte[] for java.lang.String
 193.17KB com.oracle.svm.jni                                 414.98KB java.util.HashMap$Node
 177.43KB java.util.concurrent                               214.45KB java.util.HashMap$Node[]
 144.39KB java.math                                          154.73KB java.util.concurrent.ConcurrentHashMap$Node
 125.85KB com.oracle.svm.core.reflect                        145.56KB java.lang.String[]
  94.35KB java.util.logging                                  143.73KB char[]
  91.88KB java.util.stream                                   139.85KB sun.util.locale.LocaleObjectCache$CacheEntry
      ... 116 additional packages                                 ... 778 additional object types
                                           (use GraalVM Dashboard to see all)
------------------------------------------------------------------------------------------------------------------------
                        1.0s (3.0% of total time) in 16 GCs | Peak RSS: 3.04GB | CPU load: 6.65
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /home/foivos.zakkak.net/content/tutorials/main (executable)
 /home/foivos.zakkak.net/content/tutorials/main.build_artifacts.txt
========================================================================================================================
Finished generating 'main' in 32.8s.
Warning: Image 'main' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

Note the 5th line indicating that the static initializer was ran at build-time and the Warnings that inform us that native-image failed to create a stand-alone image and that the resulting image requires a JDK for execution. Following the advise of the warning let's try to build our native image once more, this time with the --no-fallback parameter as well.

native-image --initialize-at-build-time=Main --no-fallback Main

This time the compilation fails with the following output:

========================================================================================================================
GraalVM Native Image: Generating 'main'...
========================================================================================================================
[1/7] Initializing...                                                                                    (3.1s @ 0.12GB)
Hello from the static initializer!
 Version info: 'GraalVM 22.0.0.2-Final Java 17 Mandrel Distribution'
[2/7] Performing analysis...  [*******]                                                                  (9.8s @ 0.51GB)
   2,729 (83.92%) of  3,252 classes reachable
   3,333 (60.27%) of  5,530 fields reachable
  12,343 (73.22%) of 16,857 methods reachable
      33 classes,     0 fields, and   178 methods registered for reflection
      57 classes,    58 fields, and    51 methods registered for JNI access

Error: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.  To see how this object got instantiated use --trace-object-instantiation=java.util.Random. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Detailed message:
Trace: Object was reached by
	reading field Main.random

Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
------------------------------------------------------------------------------------------------------------------------
                        0.4s (3.2% of total time) in 12 GCs | Peak RSS: 1.63GB | CPU load: 6.34
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /home/foivos.zakkak.net/content/tutorials/main.build_artifacts.txt
========================================================================================================================
Failed generating 'main' after 13.1s.
Error: Image build request failed with exit status 1

The error message makes clear that the Random instance is reached through the static field random in Main. So how do we handle this?

Handling instances of Random or SplittableRandom classes in native images

To handle such cases where a Random or SplittableRandom instance is reachable through some field of a build-time initialized or instantiated class, we have the following options:

  1. Avoid build time initialization of classes holding static fields that reference (directly or transitively) instances of Random or SplittableRandom classes. This works fine as we demonstrated by not passing --initialize-at-build-time=Main to native-image. However, we might not always be able or willing to do this. The simplest way to achieve this is to pass --initialize-at-run-time=Main to native-image and see if it works. Note that even if this works, it might impact the performance of the resulting native image since it might prevent other classes from being build-time initialized as well.
  2. Register classes holding static fields that directly reference instances of Random or SplittableRandom classes to be reinitialized at run-time. This way the referenced instance will be re-created at run-time solving the issue.

    This is often the best thing to do, as it doesn't require any special handling and allows the class causing the issue to still be build-time initialized. To achieve this one will need to define a new Feature, that will register the corresponding class (Main in our demo) for build-time initialization and run-time re-initialization, like the following:

    import org.graalvm.nativeimage.ImageSingletons;
    import org.graalvm.nativeimage.hosted.Feature;
    import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport;
    
    public class ReInitFeature implements Feature {
    
     @Override
     public void afterRegistration(AfterRegistrationAccess access) {
         RuntimeClassInitializationSupport rci = ImageSingletons.lookup(RuntimeClassInitializationSupport.class);
         rci.initializeAtBuildTime("Main", "Needs to be optimized");
         rci.rerunInitialization("Main", "Contains Random instance");
     }
    
    }

    and compile it with:

    javac ReInitFeature.java -cp ~/.sdkman/candidates/java/22.0.0.2.r17-mandrel/lib/jvmci/graal-sdk.jar:./

    Note that instead of augmenting the classpath, with the path to graal-sdk.jar in the sdkman installation, in an actual project we would use maven or gradle to pull graal-sdk as a dependency.

    The resulting Feature can be used with native-image as follows:

    native-image --no-fallback --features=ReInitFeature Main

    which will generate the following output:

    ========================================================================================================================
    GraalVM Native Image: Generating 'main'...
    ========================================================================================================================
    [1/7] Initializing...                                                                                    (2.6s @ 0.12GB)
    Hello from the static initializer!
    Version info: 'GraalVM 22.0.0.2-Final Java 17 Mandrel Distribution'
    1 user-provided feature(s)
    - ReInitFeature
    [2/7] Performing analysis...  [*******]                                                                  (9.9s @ 0.45GB)
    2,729 (83.92%) of  3,252 classes reachable
    3,333 (60.26%) of  5,531 fields reachable
    12,351 (73.23%) of 16,866 methods reachable
       33 classes,     0 fields, and   178 methods registered for reflection
       57 classes,    58 fields, and    51 methods registered for JNI access
    [3/7] Building universe...                                                                               (0.7s @ 0.64GB)
    [4/7] Parsing methods...      [*]                                                                        (0.7s @ 0.86GB)
    [5/7] Inlining methods...     [****]                                                                     (1.4s @ 0.75GB)
    [6/7] Compiling methods...    [***]                                                                     (10.6s @ 0.77GB)
    [7/7] Creating image...                                                                                  (1.6s @ 1.07GB)
    3.99MB (33.39%) for code area:    7,392 compilation units
    6.92MB (57.84%) for image heap:   1,640 classes and 91,089 objects
    1.05MB ( 8.78%) for other data
    11.96MB in total
    ------------------------------------------------------------------------------------------------------------------------
    Top 10 packages in code area:                               Top 10 object types in image heap:
    636.93KB java.util                                            1.76MB byte[] for general heap data
    300.69KB java.lang                                          792.88KB java.lang.String
    283.85KB java.text                                          587.66KB java.lang.Class
    233.44KB java.util.regex                                    487.23KB byte[] for java.lang.String
    193.17KB com.oracle.svm.jni                                 414.98KB java.util.HashMap$Node
    177.42KB java.util.concurrent                               214.45KB java.util.HashMap$Node[]
    144.39KB java.math                                          154.73KB java.util.concurrent.ConcurrentHashMap$Node
    125.91KB com.oracle.svm.core.reflect                        144.78KB java.lang.String[]
    94.35KB java.util.logging                                  143.73KB char[]
    91.34KB sun.util.locale.provider                           139.85KB sun.util.locale.LocaleObjectCache$CacheEntry
       ... 116 additional packages                                 ... 766 additional object types
                                            (use GraalVM Dashboard to see all)
    ------------------------------------------------------------------------------------------------------------------------
                         0.8s (2.7% of total time) in 16 GCs | Peak RSS: 3.08GB | CPU load: 6.54
    ------------------------------------------------------------------------------------------------------------------------
    Produced artifacts:
    /home/foivos.zakkak.net/content/tutorials/main (executable)
    /home/foivos.zakkak.net/content/tutorials/main.build_artifacts.txt
    ========================================================================================================================
    Finished generating 'main' in 29.1s.

    This will result in a successful build with Main initialized at build-time (as evident by the 5th line of the native-image output) and re-initialized at run-time as evident by the output of the generated native image:

    $ ./main
    Hello from the static initializer!
    Hello world 83 !
  3. Reset the value of fields (static or not) referencing (directly or transitively) instances of Random or SplittableRandom to null in the native-image heap.

    This is often another simple solution, but in order for it to work the code accessing the fields we reset needs to be able to handle their values being null otherwise we will get a null pointer exception (NPE) at run-time when the corresponding field gets accessed.

    import com.oracle.svm.core.annotate.Alias;
    import com.oracle.svm.core.annotate.TargetClass;
    import com.oracle.svm.core.annotate.RecomputeFieldValue;
    
    import java.util.Random;
    
    public class MainSubstitutions {
    }
    
    @TargetClass(className = "Main")
    final class Target_Main {
     @Alias //
     @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) //
     static Random random;
    }
    javac MainSubstitutions.java -cp ~/.sdkman/candidates/java/22.0.0.2.r17-mandrel/lib/svm/builder/svm.jar:./

    Note that instead of augmenting the classpath, with the path to svm.jar in the sdkman installation, in an actual project we would use maven or gradle to pull svm as a dependency.

    The resulting substitution class will be automatically picked up by native-image as long as it's in the classpath, as a result in this case running the following is enough:

    native-image --initialize-at-build-time=Main --no-fallback Main

    which will generate the following output:

    ========================================================================================================================
    GraalVM Native Image: Generating 'main'...
    ========================================================================================================================
    [1/7] Initializing...                                                                                    (2.5s @ 0.12GB)
    Hello from the static initializer!
    Version info: 'GraalVM 22.0.0.2-Final Java 17 Mandrel Distribution'
    [2/7] Performing analysis...  [*******]                                                                  (8.9s @ 0.45GB)
    2,729 (83.92%) of  3,252 classes reachable
    3,333 (60.27%) of  5,530 fields reachable
    12,343 (73.22%) of 16,857 methods reachable
       33 classes,     0 fields, and   178 methods registered for reflection
       57 classes,    58 fields, and    51 methods registered for JNI access
    [3/7] Building universe...                                                                               (0.7s @ 0.62GB)
    [4/7] Parsing methods...      [*]                                                                        (0.5s @ 0.85GB)
    [5/7] Inlining methods...     [****]                                                                     (1.1s @ 0.74GB)
    [6/7] Compiling methods...    [***]                                                                      (9.7s @ 0.67GB)
    [7/7] Creating image...                                                                                  (1.6s @ 0.97GB)
    3.99MB (33.37%) for code area:    7,388 compilation units
    6.92MB (57.84%) for image heap:   1,639 classes and 91,071 objects
    1.05MB ( 8.79%) for other data
    11.96MB in total
    ------------------------------------------------------------------------------------------------------------------------
    Top 10 packages in code area:                               Top 10 object types in image heap:
    635.83KB java.util                                            1.76MB byte[] for general heap data
    300.68KB java.lang                                          792.69KB java.lang.String
    283.85KB java.text                                          587.63KB java.lang.Class
    233.44KB java.util.regex                                    487.13KB byte[] for java.lang.String
    193.17KB com.oracle.svm.jni                                 414.98KB java.util.HashMap$Node
    177.42KB java.util.concurrent                               214.45KB java.util.HashMap$Node[]
    144.39KB java.math                                          154.73KB java.util.concurrent.ConcurrentHashMap$Node
    125.82KB com.oracle.svm.core.reflect                        144.75KB java.lang.String[]
    94.35KB java.util.logging                                  143.73KB char[]
    91.34KB sun.util.locale.provider                           139.85KB sun.util.locale.LocaleObjectCache$CacheEntry
       ... 115 additional packages                                 ... 766 additional object types
                                            (use GraalVM Dashboard to see all)
    ------------------------------------------------------------------------------------------------------------------------
                         0.7s (2.7% of total time) in 16 GCs | Peak RSS: 3.06GB | CPU load: 6.55
    ------------------------------------------------------------------------------------------------------------------------
    Produced artifacts:
    /home/foivos.zakkak.net/content/tutorials/main (executable)
    /home/foivos.zakkak.net/content/tutorials/main.build_artifacts.txt
    ========================================================================================================================
    Finished generating 'main' in 26.4s.

    As we observe the build succeeds with Main initialized at build-time (as evident by the 5th line of the native-image output) and random set to null in the generated native image as evident by running the latter:

    $ ./main
    Exception in thread "main" java.lang.NullPointerException
    at Main.main(Main.java:11)

    If this is the only way to make our application compile to native image but the code doesn't handle null values properly, we have two options:

    1. If this is our code, we can edit it to handle the null value at run time.
    2. If the code causing the issue is 3rd party code (or an earlier release of our code) that we can't alter we can substitute the methods accessing the random field and make them handle null values or even make them throw an unsupported exception if we are not interested in actually using them. Note, however, that method substitutions are very fragile and hard to maintain.

Disclaimer

This is an early version of this tutorial so there might be some rough edges :)

Foivos Zakkak
Foivos Zakkak
R&D Software Engineer

Foivos (pronounced [‘fivos]) Zakkak is a Software Engineer at Red Hat.