Native Image 基础知识

Native Image 用 Java 编写,以 Java 字节码作为输入,生成一个独立二进制文件(一个可执行文件或一个共享库)。在生成二进制文件的过程中,Native Image 可以运行用户代码。最后,Native Image 将编译后的用户代码、Java 运行时环境的一部分(例如,垃圾收集器、线程支持)以及代码执行的结果链接到二进制文件中。

我们将此二进制文件称为原生可执行文件原生镜像。我们将生成此二进制文件的实用程序称为 native-image 构建器native-image 生成器

为了清楚区分原生镜像构建期间执行的代码和原生镜像执行期间执行的代码,我们将两者之间的区别称为 构建时运行时

为了生成一个最小化镜像,Native Image 采用了一种称为 静态分析 的过程。

目录 #

构建时与运行时 #

在镜像构建期间,Native Image 可能会执行用户代码。这些代码可以产生副作用,例如将值写入类的静态字段。我们称此代码在构建时执行。通过此代码写入静态字段的值会保存到镜像堆中。运行时指的是二进制文件执行时的代码和状态。

理解这两个概念最简单的方式是通过 可配置的类初始化。在 Java 中,类在首次使用时初始化。在构建时使用的每个 Java 类都被称为构建时初始化。请注意,仅仅加载一个类并不一定会初始化它。构建时初始化类的静态类初始化器在运行镜像构建的 JVM 上执行。如果一个类在构建时初始化,其静态字段会保存在生成的二进制文件中。在运行时,首次使用此类不会触发类初始化。

用户可以通过不同方式在构建时触发类初始化

  • 通过将 --initialize-at-build-time=<class> 传递给 native-image 构建器。
  • 通过在构建时初始化类的静态初始化器中使用类。

Native Image 将在镜像构建时初始化常用的 JDK 类,例如 java.lang.Stringjava.util.** 等。请注意,构建时类初始化是一项高级功能。并非所有类都适合构建时初始化。

以下示例演示了构建时和运行时执行代码之间的区别

public class HelloWorld {
    static class Greeter {
        static {
            System.out.println("Greeter is getting ready!");
        }
        
        public static void greet() {
          System.out.println("Hello, World!");
        }
    }

  public static void main(String[] args) {
    Greeter.greet();
  }
}

将代码保存到名为 HelloWorld.java 的文件中后,我们在 JVM 上编译并运行该应用程序

javac HelloWorld.java
java HelloWorld 
Greeter is getting ready!
Hello, World!

现在我们构建它的原生镜像,然后执行

native-image HelloWorld
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
...
Finished generating 'helloworld' in 14.9s.
./helloworld 
Greeter is getting ready!
Hello, World!

HelloWorld 启动并调用了 Greeter.greet。这导致 Greeter 初始化,并打印消息 Greeter is getting ready!。在这里我们说 Greeter 的类初始化器在镜像运行时执行。

如果我们告诉 native-image 在构建时初始化 Greeter,会发生什么?

native-image HelloWorld --initialize-at-build-time=HelloWorld\$Greeter
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
Greeter is getting ready!
[1/8] Initializing...                                                                                    (3.1s @ 0.15GB)
 Java version: 24+36, vendor version: Oracle GraalVM 24+36.1
 Graal compiler: optimization level: 2, target machine: armv8.1-a, PGO: ML-inferred
 C compiler: cc (apple, arm64, 16.0.0)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
...
Finished generating 'helloworld' in 13.6s.
./helloworld 
Hello, World!

我们看到 Greeter is getting ready! 在镜像构建期间被打印出来。我们说 Greeter 的类初始化器在镜像构建时执行。在运行时,当 HelloWorld 调用 Greeter.greet 时,Greeter 已经初始化。在镜像构建期间初始化的类的静态字段存储在镜像堆中。

原生镜像堆 #

原生镜像堆,也称为镜像堆,包含

  • 在镜像构建期间创建的、可从应用程序代码访问的对象。
  • 原生镜像中使用的类的 java.lang.Class 对象。
  • 对象常量嵌入在方法代码中

当原生镜像启动时,它从二进制文件中复制初始镜像堆。

将对象包含到镜像堆中的一种方法是在构建时初始化类

class Example {
    private static final String message;
    
    static {
        message = System.getProperty("message");
    }

    public static void main(String[] args) {
        System.out.println("Hello, World! My message is: " + message);
    }
}

现在我们在 JVM 上编译并运行该应用程序

javac Example.java
java -Dmessage=hi Example
Hello, World! My message is: hi
java -Dmessage=hello Example 
Hello, World! My message is: hello
java Example
Hello, World! My message is: null

现在我们来研究一下,当构建一个 Example 类在构建时初始化的原生镜像时会发生什么

native-image Example --initialize-at-build-time=Example -Dmessage=native
================================================================================
GraalVM Native Image: Generating 'example' (executable)...
================================================================================
...
Finished generating 'example' in 19.0s.
./example 
Hello, World! My message is: native
./example -Dmessage=aNewMessage
Hello, World! My message is: native

Example 类的类初始化器在镜像构建时执行。这为 message 字段创建了一个 String 对象并将其存储在镜像堆中。

静态分析 #

静态分析是确定应用程序使用哪些程序元素(类、方法和字段)的过程。这些元素也称为可达代码。分析本身包含两部分

  • 扫描方法的字节码以确定可以从它访问的其他元素。
  • 扫描原生镜像堆中的根对象(如静态字段)以确定哪些类可从它们访问。它从应用程序的入口点(main 方法)开始。新发现的元素被迭代扫描,直到进一步扫描不再产生元素可达性的额外变化。

只有可达的元素才会被包含在最终镜像中。一旦原生镜像构建完成,在运行时不能添加新的元素,例如通过类加载。我们将此约束称为封闭世界假设

延伸阅读 #

联系我们