原生映像基礎

原生映像是以 Java 撰寫,並將 Java 位元組碼作為輸入,以產生獨立的二進位檔 (一個可執行檔或一個共享程式庫)。在產生二進位檔的過程中,原生映像可以執行使用者程式碼。最後,原生映像將已編譯的使用者程式碼、Java 執行階段的部分 (例如,垃圾回收器、執行緒支援) 和程式碼執行的結果連結到二進位檔中。

我們將此二進位檔稱為原生可執行檔原生映像。我們將產生二進位檔的公用程式稱為 native-image 建置器native-image 產生器

為了清楚區分在原生映像建置期間執行的程式碼和在原生映像執行期間執行的程式碼,我們將兩者之間的差異稱為 建置時間執行時間

為了產生最小映像,原生映像採用稱為 靜態分析 的程序。

目錄 #

建置時間 vs 執行時間 #

在映像建置期間,原生映像可能會執行使用者程式碼。此程式碼可能會有副作用,例如將值寫入類別的靜態欄位。我們說此程式碼是在建置時間執行。此程式碼寫入靜態欄位的值會儲存在 映像堆積 中。執行時間是指在執行二進位檔時的程式碼和狀態。

了解這兩個概念之間差異的最簡單方法是透過 可設定的類別初始化。在 Java 中,類別會在第一次使用時初始化。在建置時間使用的每個 Java 類別都被稱為建置時間初始化。請注意,僅載入類別不一定會初始化它。建置時間初始化的類別的靜態類別初始化器會在執行映像建置的 JVM 上執行。如果類別在建置時間初始化,其靜態欄位會儲存在產生的二進位檔中。在執行時間,第一次使用此類別不會觸發類別初始化。

使用者可以透過不同的方式在建置時間觸發類別初始化

  • --initialize-at-build-time=<class> 傳遞至 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/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 方法) 開始。會重複掃描新探索的元素,直到進一步掃描不會再導致元素的存取性發生額外變更。

只有可達元素才會包含在最終映像中。一旦建置原生映像,就無法在執行時間新增任何新元素,例如,透過類別載入。我們將此限制稱為封閉世界假設

延伸閱讀 #

與我們聯絡