返回

明確指定類別初始化

預設情況下,Native Image 會在執行階段初始化應用程式類別,但 Native Image 證明「安全」可在建置階段初始化的類別除外。不過,您可以透過明確指定要建置階段或執行階段初始化的類別,來影響預設行為。為此,有兩個命令列選項:--initialize-at-build-time--initialize-at-run-time。您可以使用這些選項來指定整個套件或個別類別。例如,如果您有類別 p.C1p.C2、…、p.Cn,您可以透過將以下選項傳遞給 native-image,指定套件 p 中的所有類別都在建置階段初始化

--initialize-at-build-time=p

如果您只想要在執行階段初始化套件 p 中的類別 C1,請使用

--initialize-at-run-time=p.C1

您也可以使用來自 Native Image Feature 介面RuntimeClassInitialization 類別,以程式設計方式指定類別初始化。

本指南示範如何透過在執行階段(預設行為)執行類別初始化器來建置原生可執行檔,然後在建置階段執行,並比較這兩種方法。

先決條件

請確認您已安裝 GraalVM JDK。最簡單的入門方法是使用 SDKMAN!。如需其他安裝選項,請造訪下載區段。

執行範例

在此範例中,執行一個簡單的 Java 應用程式,該應用程式會剖析 2023 年的一些 Java 演講。剖析器會建立記錄,並將它們新增至 List<Talk> 集合。

  1. 將以下 Java 原始碼儲存至名為 TalkParser.java 的檔案中
     import java.util.ArrayList;
     import java.util.List;
     import java.util.Scanner;
    
     public class TalkParser {
       private static final List<Talk> TALKS = new ArrayList<>();
       static {
         Scanner s = new Scanner("""
             Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
             Anatomy of a Spring Boot App with Clean Architecture by Steve Pember
             Java in the Cloud with GraalVM by Alina Yurenko
             Bootiful Spring Boot 3 by Josh Long
             """);
         while (s.hasNextLine()) {
           TALKS.add(new Talk(s.nextLine()));
         }
         s.close();
       }
    
       public static void main(String[] args) {
         System.out.println("Talks loaded using scanner:");
         for (Talk talk : TALKS) {
             System.out.println("- " + talk.name());
         }
       }
     }
    
     record Talk (String name) {}
    
  2. 編譯應用程式
     javac TalkParser.java
    
  3. 建置原生可執行檔,明確地在執行階段執行類別初始化器
     native-image --initialize-at-run-time=TalkParser,Talk -o runtime-parser TalkParser
    

    您可以在此範例中省略 --initialize-at-run-time=TalkParser,Talk 選項,因為這些類別預設會標記為在執行階段初始化。-o 選項指定輸出檔案的名稱。

  4. 執行和 time 原生應用程式
     time ./runtime-parser
    

    在具有 16 GB 記憶體和 8 個核心的電腦上,您應該會看到類似以下的結果:``` 使用掃描器載入的演講

    • Venkat Subramaniam 的 Java 非同步程式設計:可供選擇的選項
    • Steve Pember 的採用乾淨架構的 Spring Boot 應用程式剖析
    • Alina Yurenko 的在雲端中使用 GraalVM 的 Java
    • Josh Long 的 Bootiful Spring Boot 3 ./runtime-parser 0.00s 使用者 0.00s 系統 52% cpu 0.010 總計 ``` 應用程式會在執行階段剖析文字區塊。

    檢查檔案大小,應該約為 13M

     du -sh runtime-parser
    
  5. 接下來,建置一個原生可執行檔,在建置階段初始化 TalkParser,並為輸出檔案提供不同的名稱,以與先前的組建區別開來。Talk 記錄也必須明確初始化,因此此類型的物件會持續保留在可執行堆積中。
    native-image --initialize-at-build-time=TalkParser,Talk -o buildtime-parser TalkParser
    

    如果您的應用程式將其他類型新增至映像堆積,則必須明確標記每個類型(或對應的套件)以進行建置階段初始化。適當的可操作錯誤訊息將引導您完成此流程。

  6. 執行並 time 第二個可執行檔以進行比較
     time ./buildtime-parser
    

    這次您應該會看到類似以下的內容

     Talks loaded using scanner:
     - Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
     - Anatomy of a Spring Boot App with Clean Architecture by Steve Pember
     - Java in the Cloud with GraalVM by Alina Yurenko
     - Bootiful Spring Boot 3 by Josh Long
     ./buildtime-parser  0.00s user 0.00s system 53% cpu 0.016 total
    

    檢查檔案大小,應該會減少到約 6.4M!

     du -sh buildtime-parser
    

    檔案大小變更是因為 Native Image 在建置階段執行靜態初始化器、剖析文字區塊,並僅將 Talk 記錄保存在可執行檔中。

    因此,當 Native Image 靜態分析應用程式時,大部分的掃描基礎結構不會變成可到達的,因此不會包含在可執行檔中。

另一個用於更精確地分析應用程式的有價值標準是指令數量,可以使用 Linux perf 分析器取得。

例如,對於此範例應用程式,在建置階段類別初始化的情況下,指令數量減少了近 30%(從 1180 萬減少到 860 萬)

perf stat ./runtime-parser 
Talks loaded using scanner:
- Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
(...)
 Performance counter stats for './runtime-parser':
(...)                   
        11,323,415      cycles                           #    3.252 GHz                       
        11,781,338      instructions                     #    1.04  insn per cycle            
         2,264,670      branches                         #  650.307 M/sec                     
            28,583      branch-misses                    #    1.26% of all branches           
(...)   
       0.003817438 seconds time elapsed
       0.000000000 seconds user
       0.003878000 seconds sys 
perf stat ./buildtime-parser 
Talks loaded using scanner:
- Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
(...)
 Performance counter stats for './buildtime-parser':
(...)                    
         9,534,318      cycles                           #    3.870 GHz                       
         8,609,249      instructions                     #    0.90  insn per cycle            
         1,640,540      branches                         #  665.818 M/sec                     
            23,490      branch-misses                    #    1.43% of all branches           
(...)
       0.003119519 seconds time elapsed
       0.001113000 seconds user
       0.002226000 seconds sys 

這示範了 Native Image 如何將工作從執行階段轉移到建置階段:當類別在建置階段初始化時,會在建置可執行檔時剖析文字區塊,並且僅包含已剖析的物件。這不僅可以使可執行檔的檔案大小變小,而且執行速度更快:當可執行檔執行時,Talk 記錄已經存在,只需要列印即可。

為了確保使用 Native Image 建置的原生可執行檔盡可能與 HotSpot 行為相容,無法在建置階段安全初始化的應用程式類別會在執行階段初始化。您身為使用者,或您使用的架構,必須明確要求某些類別進行建置階段初始化,才能從較小的檔案大小和更快的執行時間中獲益。請包含正確的資料結構,以避免映像大小暴增。我們也建議僅將 --initialize-at-build-time 與單一類別搭配使用。您可能需要新增許多 --initialize-at-build-time 項目。請注意,不正確的建置階段初始化可能會導致在生產環境中應避免的問題,例如功能異常或包含機密資料(例如密碼或加密金鑰)。

結論

本指南示範了如何影響預設的 native-image 類別初始化原則,並將其設定為根據使用案例在建置階段初始化特定類別。建置階段與執行階段初始化的優點會在Native Image 中的類別初始化中說明,但簡而言之,正確使用建置階段初始化可以顯著減少整體檔案大小,並改善應用程式的執行階段。

與我們聯繫