返回

显式指定类初始化

默认情况下,Native Image 会在运行时初始化应用程序类,除了 Native Image 证明在构建时“安全”初始化的类之外。但是,您可以通过显式指定要在构建时或运行时初始化的类来影响默认行为。为此,有两个命令行选项:--initialize-at-build-time--initialize-at-run-time。您可以使用这些选项来指定整个包或单个类。例如,如果您有类 p.C1p.C2、…、p.Cn,您可以指定包 p 中的所有类都将在构建时初始化,方法是将以下选项传递给 native-image

--initialize-at-build-time=p

如果您只想在运行时初始化包 p 中的类 C1,请使用

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

您还可以使用 RuntimeClassInitializationNative Image 功能接口 中以编程方式指定类初始化。

本指南演示了如何在运行时(默认行为)和构建时运行类初始化程序来构建本机可执行文件,并比较了这两种方法。

先决条件

确保您已安装 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 个内核的机器上,您应该会看到类似以下的结果: ``` 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 ./runtime-parser 0.00s user 0.00s system 52% cpu 0.010 total ``` 该应用程序在运行时解析文本块。

    检查文件大小,应该大约为 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 在构建时运行静态初始化程序,解析文本块,并将解析后的对象持久化到可执行文件中。

    因此,当 Native Image 静态分析应用程序时,大多数扫描基础结构不会变得可访问,因此不会包含在可执行文件中。

另一个更准确地分析应用程序的有价值标准是指令数量,可以使用 Linux perf 分析器 获得。

例如,对于此演示应用程序,在构建时类初始化的情况下,指令数量减少了近 30%(从 11.8M 减少到 8.6M)

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 中的类初始化 中有所描述,但简而言之,构建时初始化可以显着减少整体文件大小并提高应用程序的运行时性能(如果使用正确)。

联系我们