返回

明确指定类初始化

默认情况下,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 核的机器上,您应该会看到类似以下的结果:``` 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 在构建时运行静态初始化器,解析文本块,并仅将 Talk 记录持久化到可执行文件中。

    因此,当 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 中的类初始化中进行了描述,但简而言之,正确使用构建时初始化可以显著减小整体文件大小并提高应用程序的运行时性能。

联系我们