返回

使用构建报告优化原生可执行文件的大小

您可以通过利用 Native Image 提供的不同工具来优化您的原生可执行文件。本指南演示了如何使用构建报告工具来更好地理解生成原生可执行文件的内容,以及应用程序中一个微小的改动(在不改变语义的情况下)如何影响最终二进制文件的大小。

注意:构建报告在 GraalVM 社区版中不可用。

先决条件

请确保您已安装 GraalVM JDK。最简单的入门方法是使用SDKMAN!。有关其他安装选项,请访问下载部分

对于演示,您将运行一个简单的 Java 应用程序,它从输入字符串中提取第 i 个单词。单词由逗号分隔,并可能被任意数量的空白字符包围。

  1. 将以下 Java 代码保存到名为 IthWord.java 的文件中
    public class IthWord {
      public static String input = "foo     \t , \t bar ,      baz";
    
       public static void main(String[] args) {
           if (args.length < 1) {
               System.out.println("Word index is required, please provide one first.");
               return;
           }
           int i = Integer.parseInt(args[0]);
    
           // Extract the word at the given index.
           String[] words = input.split("\\s+,\\s+");
           if (i >= words.length) {
               System.out.printf("Cannot get the word #%d, there are only %d words.%n", i, words.length);
               return;
           }
    
           System.out.printf("Word #%d is %s.%n", i, words[i]);
       }
    }
    
  2. 编译应用程序
    javac IthWord.java
    

    (可选)使用任意参数测试应用程序以查看结果

    java IthWord 1
    

    输出应为

    Word #1 is bar.
    
  3. 从类文件构建原生可执行文件以及构建报告
    native-image IthWord --emit build-report
    

    该命令会在当前工作目录中生成一个可执行文件 ithword。构建报告文件 ithword-build-report.html 会自动与原生可执行文件一起创建。报告的链接也会列在构建输出末尾的 Build artifacts 部分。您可以通过在 build-report 选项后面附加文件名或路径来指定不同的报告文件名或路径,例如 --emit build-report=/tmp/custom-name-build-report.html

    (可选)使用相同的参数运行此可执行文件

    ./ithword 1
    

    输出应与之前相同

    Word #1 is bar.
    
  4. 构建报告是一个 HTML 文件。在浏览器中打开报告。首先,您会看到关于镜像构建的概要信息。您可以在右上方的 Image Details 图表上方看到总镜像大小

    Initial Summary

    初始大小符合预期,但作为参考,一个 HelloWorld 应用程序的大小约为 7 MB。因此,尽管代码相当简单,差异仍然很大。继续进行调查。

  5. 通过点击导航中的选项卡或图表中相应的条形图,转到 Code Area 选项卡。

    您现在看到的分解图表以字节码大小的形式可视化了不同包之间的关系。请注意,所示包仅包含被静态分析发现的可达方法。这意味着所示包(及其类)是最终被编译并包含在最终二进制文件中的唯一部分。

    Initial Code Breakdown

    您可以得出的第一个结论是,大部分代码源于 JDK 或 Native Image 内部代码——请注意,IthWord 类仅占所有可达方法总字节码大小的 0.013%。

  6. 只需点击 java 包即可深入查看。大多数可达代码(几乎一半)来自 java.util 包。此外,您可能会注意到 java.textjava.time 包贡献了 java 包大小的近 20%。但应用程序真的使用了这些包吗?

    Initial Code Breakdown - Drill-Down To Java Package

  7. 深入查看 text

    Initial Code Breakdown - Drill-Down To Text Package

    您现在可以看到,大多数可达类都用于文本格式化(请参阅下面的包和类列表)。到目前为止,您可以怀疑所包含的格式化类只能从一个地方(尽管没有实际使用)变得可达:System.out.printf

  8. 回到 java 包(通过点击中心圆圈或图表顶部的 java 名称)。

  9. 接下来深入查看 time

    Initial Code Breakdown - Drill-Down To Time Package

    近一半的包大小来自其 format 子包(类似于 java.text 包中的情况)。因此,System.out.printf 是您改进二进制文件大小的第一个机会。

  10. 返回初始应用程序,只需将 System.out.printf 切换为 System.out.println
    public class IthWord {
        public static String input = "foo     \t , \t bar ,      baz";
    
        public static void main(String[] args) {
            if (args.length < 1) {
                System.out.println("Word index is required, please provide one first.");
                return;
            }
            int i = Integer.parseInt(args[0]);
    
            // Extract the word at the given index.
            String[] words = input.split("\\s+,\\s+");
            if (i >= words.length) {
                // Use System.out.println instead of System.out.printf.
                System.out.println("Cannot get the word #" + i + ", there are only " + words.length + " words.");
                return;
            }
    
            // Use System.out.println instead of System.out.printf.
            System.out.println("Word #" + i + " is " + words[i] + ".");
        }
    }
    
  11. 重复步骤 2-4(编译类文件、构建原生可执行文件并打开新报告)。

  12. Summary 部分可以看到,总二进制大小减少了近 40%

    Second Summary

  13. 再次转到 Code Area 选项卡并深入查看 java 包。您可以看到最初的假设是正确的:java.textjava.time 包都不再可达。

    Second Code Breakdown - Java Drill-Down

    继续查看是否有应用程序不一定需要的更多可达代码。

    正如您可能已经猜到的,另一个候选项位于 java.util 包中,是 regex 子包。该包现在单独贡献了 java 包大小的近 15%。请注意,正则表达式(\\s+,\\s+)用于将原始输入拆分成单词。虽然非常方便,但它使得前面提到的 regex 包成为不必要的依赖。正则表达式本身并不复杂,可以有不同的实现方式。

  14. 接下来转到 Image Heap 选项卡继续我们的探索。此部分提供了构成镜像堆的所有对象类型的列表:该堆包含可达对象,如静态应用程序数据、元数据和用于不同目的的字节数组。在这种情况下,列表看起来很正常:大部分大小来自存储在专用字节数组中的原始字符串值(约 20%)、StringClass 对象(约 20%),以及代码元数据(20%)。

    Heap Breakdown

    此应用程序中没有特定对象类型对镜像堆有很大贡献。但有一个意料之外的条目:一小部分大小贡献(约 2%)是由于嵌入到镜像堆中的资源。应用程序没有使用任何显式资源,因此这是意料之外的。

  15. 切换到 Resource 选项卡继续调查。此部分提供了通过配置文件明确请求的所有资源的列表。还有选项可以切换其他类型的资源(缺失资源、注入资源和目录资源);但是,这超出了本指南的范围。在Native Image 构建报告中了解更多信息。

    Initial Resources

    总结这部分,只有一个资源(java/lang/uniName.dat)来自 java.base 模块,它也对镜像堆有所贡献,但并未从应用程序代码中明确请求。您对此无能为力,但请记住 JDK 代码(从用户代码间接可达)也可能使用额外的资源,这会对其大小产生不利影响。

  16. 现在回到应用程序代码,实现一种不使用正则表达式的新方法。以下代码使用 String.substringString.indexOf 来保持语义,同时保持逻辑相对简单
    public class IthWord {
        public static String input = "foo     \t , \t bar ,      baz";
    
        public static void main(String[] args) {
            if (args.length < 1) {
               System.out.println("Word index is required, please provide one first.");
               return;
            }
            int i = Integer.parseInt(args[0]);
    
            // Extract the word at the given index using String.substring and String.indexOf.
            String word = input;
            int j = i, index;
            while (j > 0) {
               index = word.indexOf(',');
               if (index < 0) {
                   // Use System.out.println instead of System.out.printf.
                   System.out.println("Cannot get the word #" + i + ", there are only " + (i - j + 1) + " words.");
                   return;
               }
               word = word.substring(index + 1);
               j--;
            }
            index = word.indexOf(',');
            if (index > 0) {
               word = word.substring(0, word.indexOf(','));
            }
            word = word.trim();
    
            // Use System.out.println instead of System.out.printf.
            System.out.println("Word #" + i + " is " + word + ".");
        }
    }
    
  17. 再次重复步骤 2-4(编译类文件、构建原生可执行文件并打开新报告)。

  18. Summary 部分,您可以看到总二进制大小再次得到了改进(约 15%)

    Final Summary

    此外,先前注册的资源不再是生成二进制文件的一部分(再次查看 Resources 部分以确认)

    Final Resources

本指南演示了如何使用构建报告优化原生可执行文件的大小。构建报告允许您更详细地探索生成原生可执行文件的内容。更好地理解哪些代码是可达的,使您能够以一种在保持其语义的同时移除不必要的 JDK 依赖项的方式实现应用程序。

联系我们