原生镜像基础
原生镜像是用 Java 编写的,它以 Java 字节码作为输入,生成一个独立的二进制文件(一个**可执行文件**或一个**共享库**)。在生成二进制文件的过程中,原生镜像可以运行用户代码。最后,原生镜像将编译后的用户代码、部分 Java 运行时(例如,垃圾收集器、线程支持)以及代码执行的结果链接到二进制文件中。
我们将此二进制文件称为**原生可执行文件**或**原生镜像**。我们将生成二进制文件的工具称为**native-image
构建器**或**native-image
生成器**。
为了清楚地区分原生镜像构建期间执行的代码和原生镜像执行期间执行的代码,我们将两者之间的差异称为构建时和运行时。
为了生成最小镜像,原生镜像使用一种称为静态分析的过程。
目录 #
构建时与运行时 #
在镜像构建期间,原生镜像可能会执行用户代码。此代码可能具有副作用,例如将值写入类的静态字段。我们说此代码是在构建时执行的。此代码写入静态字段的值将保存在镜像堆中。运行时指的是二进制文件在执行时的代码和状态。
理解这两个概念之间差异的最简单方法是通过可配置的类初始化。在 Java 中,类在第一次使用时初始化。在构建时使用的每个 Java 类都被称为构建时初始化。请注意,仅仅加载类不一定意味着初始化它。构建时初始化类的静态类初始化器在**运行镜像构建的 JVM 上执行**。如果类在构建时初始化,其静态字段将保存在生成的二进制文件中。在运行时,第一次使用此类不会触发类初始化。
用户可以通过多种方式在构建时触发类初始化
- 通过向
native-image
构建器传递--initialize-at-build-time=<class>
。 - 通过在构建时初始化类的静态初始化器中使用类。
原生镜像将在镜像构建时初始化常用的 JDK 类,例如 java.lang.String
、java.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/7] Initializing... (3.1s @ 0.15GB)
Version info: 'GraalVM dev Java 11 EE'
Java version info: '11.0.15+4-jvmci-22.1-b02'
C compiler: gcc (linux, x86_64, 9.4.0)
Garbage collector: Serial GC
...
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
方法)开始。新发现的元素会不断被扫描,直到进一步扫描不再导致元素可达性的任何变化为止。
只有可达元素会被包含在最终镜像中。构建原生镜像后,在运行时无法添加任何新元素,例如通过类加载。我们将这种约束称为封闭世界假设。