Skip to content

Class Initialization in qbicc

David Grove edited this page May 26, 2022 · 5 revisions

Overview

One of the goals of the qbicc project is to explore technical approaches for adapting Java's specification of class initialization to fully support native image compilation. Enabling build-time evaluation of complex class initialization logic is essential for obtaining much of the benefits of native image compilation: reduced memory footprint and fast startup. However, both the core JDK and many frameworks will not be primarily be used in native image scenarios. Therefore, it is essential that the approach taken for build-time initialization enables both the existing runtime class initialization and the new build-time class initialization logic to co-exist. Furthermore, for as many cases as possible, the class initialization code should be shared between the two usage scenarios and have non-surprising semantics in both.

Build-time Initialization

In qbicc, all classes are initialized at build-time. Class initialization at build time is performed according to the existing semantics of Java class initialization driven by build-time execution of the <clinit> methods of reachable classes. The set of reachable classes is determined iteratively, starting with the program entrypoints and adding the methods and classes they utilize until no further reachable classes are discovered (a fixed point is reached).

After build-time initialization has completed, a build-time heap has been constructed that contains the objects that were created during the build-time execution of the <clinit> methods. Using the reachable static fields of the reachable program as roots, this build-time heap is serialized into the native image. This set of objects will form the initial runtime heap of the program when it is executed.

Runtime Initializers

There are cases where one or more initialization actions of a class must be executed at program runtime. Most typically these involve the creation of native resources (open files, threads, etc) that cannot be successfully serialized into the build time heap.

Qbicc supports runtime initialization by allowing static fields of a classes to be declared as runtime initialized. These fields will be initialized lazily, at first access, by executing a runtime initializer (<rtinit>) associated with the accessed field. Runtime initialization is localized: accessing a particular static field will cause its runtime initializer to be executed but has no implications for other runtime initializers defined either in the field's defining class or any superclass or implemented interface of the field's defining class.

When serialized from the build-time heap to the runtime heap, all runtime-initialized fields will be serialized with the zero (uninitialized) value appropriate for their type.

Qbicc allows related static fields in the same class to share a common <rtinit> method. The first access to any of the fields will cause the execution of the associated <rtinit> method and the initialization of all the fields.

Adjusting Heap Serialization

For some objects it is necessary to initialize them during build-time initialization, but "reset" them before they are used at runtime. Qbicc supports this by allowing fields to be annotated to be serialized as the type-appropriate zero value or as a primitive constant value. This value replacement happens as the build time heap is serialize.

One common scenario is to invalidate objects that are wrapping native resources. For example, when a FileDescriptor is serialized its fd and handle instance fields are serialized as -1 and its closed field is serialize as true. Thus, any attempt to use the build-time FileDescriptor at runtime will raise the appropriate exception.

Patching: Migration for Existing Classes

The runtime initialization mechanisms described above are currently enabled via a set of annotations. This allows qbicc to implement the desired semantics without requiring any changes to the Java compiler, class file format, or language specification. In the long term, we believe small modifications to the Java specification, for example defining a rtinit { ... } similar to the existing static { ... } construct could enable a simpler specification.

The primary annotation for runtime initialization is RuntimeAspect. This annotation is defined on a class and is interpreted as meaning that the <clinit> method of the class should be interpreted as an <rtinit> method. This method will not be executed during build-time initialization and instead will be deferred until the first access of one of the static fields defined in the class.

To allow us to "externally" modify JDK core classes for qbicc, we have developed an annotation-driven patcher infrastructure. The patcher allows the declaration of patch classes that add, remove, and modify the methods and fields of an existing class. This modification includes the replacement of the <clinit> method and the declaration of multiple RuntimeAspect patch classes.

The best way to explore what is possible with the patcher is to examine the java.base/src directory in the qbicc-class-library project. It makes extensive use of the patcher annotations to adapt the core JDK classes to qbicc while still allowing us to consume the upstream OpenJDK code base via an unmodified git submodule.

Design Alternatives

A number of alternatives were considered before arriving at the final design documented here. The technical discussions and options considered can be explored starting in https://github.com/qbicc/qbicc/discussions/764.