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 classB
has a field (static
or not) of typeA
, or of a subtype ofA
.
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.
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:
- Avoid build time initialization of classes holding
static
fields that reference (directly or transitively) instances ofRandom
orSplittableRandom
classes. This works fine as we demonstrated by not passing--initialize-at-build-time=Main
tonative-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
tonative-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. -
Register classes holding
static
fields that directly reference instances ofRandom
orSplittableRandom
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 withnative-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 thenative-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 !
-
Reset the value of fields (
static
or not) referencing (directly or transitively) instances ofRandom
orSplittableRandom
tonull
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 thenative-image
output) andrandom
set tonull
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:- If this is our code, we can edit it to handle the
null
value at run time. - 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 handlenull
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.
- If this is our code, we can edit it to handle the
Disclaimer
This is an early version of this tutorial so there might be some rough edges :)