- Native Image : Class Initialization
- Build-Time Initialization of Native Image Runtime
- Automatic Initialization of Safe Classes
One of the most misunderstood features of native image is the class initialization strategy. Here we'll try to explain it a little.
- Classes need to be initialized before they can be used
- The lifecycle of native image is split into
two parts:
- Build time
- Run time.
- Build Time Intitialised Classes:
- Important JDK Class, GC & depotimiser
- Known safe classes are initialised at Build Time
- By default other classes are initialized at runtime
- But you can initialize at buildtime - if you want to
We will look into this in more detail in the following.
Let's explore an example application that consists of a few classes to better understand the implications of
initialisation at run-time
or build-time
and how we can configure the initialisation strategy.
Here's our program - you can create these files to follow along, create it in a file called, Main0.java
:
import java.nio.charset.*;
public class Main0 {
public static void main(String[] args) {
A.b.doit();
}
}
class A {
public static B b = new B();
}
class B {
private static final Charset UTF_32_LE = Charset.forName("UTF-32LE");
public void doit() {
System.out.println(UTF_32_LE);
}
}
It consists of 3 classes: Main, calling A.b.doit()
; A, holding a reference to a B instance in a static field; and B,
holding a reference to a Charset - UTF-32LE
.
Run it:
# Compile it
javac Main0.java
# And run it
java Main0
Now build a native image of it, run it, and take a look at what happens:
native-image -cp . Main0
./main0
It breaks, with the following exception, because the Charset UTF_32_LE
is not by default included in the native image and is thus not found at runtime. Note Not all Charsets are added to native images, adding them is a deliberate step and this helps to reduce image size:
Exception in thread "main" java.lang.ExceptionInInitializerError
at com.oracle.svm.core.classinitialization.ClassInitializationInfo.initialize(ClassInitializationInfo.java:291)
at A.<clinit>(Main.java:15)
at com.oracle.svm.core.classinitialization.ClassInitializationInfo.invokeClassInitializer(ClassInitializationInfo.java:351)
at com.oracle.svm.core.classinitialization.ClassInitializationInfo.initialize(ClassInitializationInfo.java:271)
at Main.main(Main.java:7)
Caused by: java.nio.charset.UnsupportedCharsetException: UTF-32LE
at java.nio.charset.Charset.forName(Charset.java:529)
at B.<clinit>(Main.java:21)
at com.oracle.svm.core.classinitialization.ClassInitializationInfo.invokeClassInitializer(ClassInitializationInfo.java:351)
at com.oracle.svm.core.classinitialization.ClassInitializationInfo.initialize(ClassInitializationInfo.java:271)
... 4 more
One way to resolve this issue is to include all charsets:
native-image -H:+AddAllCharsets -cp . Main0
We're more interested in the class init details right now and adding all of the charsets may not be the best way
to solve our problem. Let's use the -H:+PrintClassInitialization
to check how the classes are initialized:
native-image -H:+PrintClassInitialization -cp . Main0
Check the output:
cat reports/build_time_classes_*
You can for example see classes such as:
com.oracle.svm.core.containers.cgroupv1.CgroupV1Subsystem
com.oracle.svm.core.containers.cgroupv2.CgroupV2Subsystem
Which are used for determining the V1/V2 cgroup resources availability when running in containers.
And also our classes A
and B
. Initializing the class means running it's <clinit>
so it tries to load the charset
and it breaks at runtime.
What if we move the initialization of these classes to build time? This will succeed because build time is a Java process and it'll load the charset without any problems - all of the charsets are avilable to the Java process that is doing the native image build.
native-image --no-fallback --initialize-at-build-time=A,B -cp . Main0 main0-works
The classes are initialized at build time, the Chatset instance is written out to the image heap and can be used at runtime.
Run the native image application again, in order to confirm that this now does work as expected:
./main0-works
Sometimes objects instantiated during the build class initialization cannot be initialised ar build-time
and written
to the image heap.
When classes contain any of the following, they can't be written to the image heap:
- Opened files
- Running threads
- Opened network sockets
- Random instances
If the analysis sees them in the image heap - it'll notify you and ask you to initalize the classes holding them at
runtime, but be aware that there may be a chain of dependencies that caused that class to be initialised at build-time
.
For example, if we modify the code to be as below, what will happen when we build? In fact, let's create a new file,
Main1.java
, to hold our updated code.
import java.nio.charset.*;
public class Main1 {
public static void main(String[] args) {
A.b.doit();
System.out.println(A.t);
}
}
class A {
public static B b = new B();
public static Thread t;
static {
// Oh no! We added a Thread
// This is something that can't go onto the Image Heap
t = new Thread(()-> {
try {
Thread.sleep(30_000);
} catch (Exception e){}
});
t.start();
}
}
class B {
private static final Charset UTF_32_LE = Charset.forName("UTF-32LE");
public void doit() {
System.out.println(UTF_32_LE);
}
}
Compile this code:
javac Main1.java
Building the native image like before will now fail, but please notice the build error:
native-image --no-fallback --initialize-at-build-time=A,B -cp . Main1 main1-fails
What happened when we ran this? This is the error you will see, when you try and build the Native Image now:
Error: Detected a started Thread in the image heap. Threads running in the image generator are no longer running at image run time. To see how this object got instantiated use -H:+TraceClassInitialization. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image run time 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 A.t
Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1
Balancing initialization can be a bit tricky, so by default GraalVM initializes classes at runtime. So for this example
it's good to have initialize only B
at build time.
native-image --no-fallback --initialize-at-build-time=B -cp . Main1 main1-b-at-buildtime
This now builds and we can run it. Running it will take 30 seconds now because of the added Thread.sleep
./main1-b-at-buildtime
UTF-32LE
Thread[Thread-0,5,main]
~/init-strategy
Correspondingly, you can use the --initialize-at-run-time=package.C1
option to make classes init at runtime.
Next, we'll try to explore various deployment options for native images.