IE盒子

帖子
查看: 85|回复: 0

简单几步,把Java代码编译成二进制文件!

[复制链接]

2

主题

7

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2023-2-7 14:12:10 | 显示全部楼层 |阅读模式
众所周知,Java作为一门极其成功的面向对象语言,拥有着相对简单的语法,严格的面向对象机制,以及半编译半解释型语言一次编译到处运行,并且速度相对较快的优势。依靠着众多优势,以及人数庞大的开发者生态群体,使得Java语言在诞生以来20多年间始终在各个编程领域保持统治地位。
但Java作为一门仍然需要解释运行的语言,仍然有一些无法逃避的弱点。比如,由于类加载机制,导致Java应用的冷启动速度较慢,在服务端领域较难应对初始的高发流量请求,再有就是Java应用在docker等容器环境中部署,需要带上JDK打包等等。
如果能把Java应用直接像C/C++代码一样直接编译出可执行的二进制文件,以上的缺点就会完全消除。正是因为需要满足这样的需求,Oracle基于Java虚拟机的特点开发出了他们称之为native-image的二进制编译技术,以及包含这个技术的GraalVM。基于GraalVM,进行简单配置即可将Java字节码完全编译成独立可执行的二进制文件。
简单提一句Native-Image编译的原理:
1. native-image 工具对代码进行静态分析,以确定所需要的Java运行时类和方法。
2. 将类、方法和资源编译成二进制文件。
由Native-Image生成的可执行文件有几个重要的优点:

  • 仅使用了Java虚拟机所需资源的一小部分,占用资源更少
  • 启动时间仅需几毫秒,启动后立即提供最佳性能
  • 可直接打包成轻量级容器,方便快速高效部署
  • 增加安全性和反编译难度
那么说了这么多编译成二进制的优势,具体该怎么做呢?我们需要借助GraalVM来完成整个过程。
首先,需要到Graal官网下载一份GraalVM并解压:
解压好后打开其中的bin文件夹,在这里启动终端并执行:
gu install native-imagenative-image插件就会安装到GraalVM的bin目录中。
由于二进制编译还是需要相应的编译器,在进行编译之前还需要安装好依赖。 对于Linux需要安装如下依赖:
# Ubuntu或其他apt包管理器
$ sudo apt-get install build-essential libz-dev zlib1g-dev
# 使用yum或dnf的包管理器
$ sudo dnf install gcc glibc-devel zlib-devel libstdc++-static在macOS上则需要安装xcode。
在Windows环境需要安装Visual Studio或者Visual Studio Build Tools,版本2017或以上。
配好依赖后,就可以开始编译了。Native-Image以Java字节码或jar包作为输入,输出一个完整的二进制文件:
# 以class文件作为输入
native-image [选项] [类名] [输出image的名字] [输出选项]
# 以jar文件作为输入
native-image [选项] -jar xxx.jar [输出image的名字]我们先找一些原生Java代码试试。自己写了一段简单代码,遍历从根节点开始的所有文件然后排序并树形打印。IDEA里运行成功后,拿到生成的class文件输入Native-Image并编译:
┌──(root㉿kali)-[/graalvm-ee-java17-22.3.0/bin]
└─# ./native-image FileTree FileTree --gc='G1'
====================================================================================================
GraalVM Native Image: Generating 'FileTree' (executable)...
====================================================================================================
[1/7] Initializing...                                                               (14.0s @ 0.22GB)
Version info: 'GraalVM 22.3.0 Java 17 EE'
Java version info: '17.0.5+9-LTS-jvmci-22.3-b07'
C compiler: gcc (linux, x86_64, 11.3.0)
Garbage collector: G1 GC
[2/7] Performing analysis...  [*****]                                               (10.5s @ 0.83GB)
   2,088 (64.31%) of  3,247 classes reachable
   1,918 (43.26%) of  4,434 fields reachable
   8,092 (36.07%) of 22,434 methods reachable
      20 classes,     0 fields, and   258 methods registered for reflection
      50 classes,    34 fields, and    48 methods registered for JNI access
       9 native libraries: dl, g1gc-cr, m, pthread, rt, stdc++, z
[3/7] Building universe...                                                           (2.8s @ 0.42GB)
[4/7] Parsing methods...      
  •                                                     (1.1s @ 0.81GB)
    [5/7] Inlining methods...     [***]                                                  (0.5s @ 0.26GB)
    [6/7] Compiling methods...    [****]                                                (17.9s @ 1.30GB)
    [7/7] Creating image...                                                              (2.3s @ 1.53GB)
       3.59MB (38.77%) for code area:     3,931 compilation units
       3.64MB (39.40%) for image heap:   50,147 objects and 2 resources
       2.02MB (21.83%) for other data
       9.25MB in total
    ----------------------------------------------------------------------------------------------------
    Top 10 packages in code area:                     Top 10 object types in image heap:
    457.11KB java.lang                                552.43KB byte[] for code metadata
    353.61KB java.util                                423.16KB byte[] for java.lang.String
    319.12KB com.oracle.svm.core.code                 338.91KB java.lang.String
    216.35KB java.util.regex                          313.88KB java.lang.Class
    167.69KB java.util.concurrent                     263.73KB byte[] for general heap data
    124.87KB java.io                                  148.69KB byte[] for embedded resources
    109.48KB java.math                                141.42KB char[]
    103.33KB java.lang.invoke                         135.06KB java.util.HashMap$Node
      98.47KB jdk.internal.icu.impl                     85.02KB java.util.HashMap$Node[]
      97.71KB com.oracle.svm.core                       81.56KB c.o.svm.core.hub.DynamicHubCompanion
       1.56MB for 97 more packages                     552.11KB for 535 more object types
    ----------------------------------------------------------------------------------------------------
                  0.7s (1.0% of total time) in 22 GCs | Peak RSS: 2.61GB | CPU load: 3.80
    ----------------------------------------------------------------------------------------------------
    Produced artifacts:
    /graalvm-ee-java17-22.3.0/bin/FileTree (executable)
    /graalvm-ee-java17-22.3.0/bin/FileTree.build_artifacts.txt (txt)
    ====================================================================================================
    Finished generating 'FileTree' in 51.2s.

    ┌──(root㉿kali)-[/graalvm-ee-java17-22.3.0/bin]
    └─# file FileTree
    FileTree: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7ed850611386ec7475a845172c919804204c6ffe, for GNU/Linux 3.2.0, not stripped把生成的二进制文件拿来反复运行了几次,对比内存占用上,用OpenJDK直接执行字节码,最高在990M~1.1G之间,而二进制Image则在680M~830M之间。在速度上两边似乎没有太大差别,都能在6秒左右扫描完成开始打印文件树,运行的结果也没有异常或错误。
    接下来测试jar包,我找来了之前已经打成jar包的一个SpringBoot工程,尝试直接编译成本地镜像看看是否可以:
    ┌──(root㉿kali)-[/graalvm-ee-java17-22.3.0/bin]
    └─# ./native-image -jar file_provider-1.1.jar file_provider --no-fallback --gc='G1'
    =====================================================================================================
    GraalVM Native Image: Generating 'file_provider' (executable)...
    =====================================================================================================
    [1/7] Initializing...                                                                 (5.4s @ 0.23GB)
    Version info: 'GraalVM 22.3.0 Java 17 EE'
    Java version info: '17.0.5+9-LTS-jvmci-22.3-b07'
    C compiler: gcc (linux, x86_64, 11.3.0)
    Garbage collector: G1 GC
    [2/7] Performing analysis...  [******]                                               (17.8s @ 1.32GB)
       3,655 (76.05%) of  4,806 classes reachable
       4,531 (52.55%) of  8,622 fields reachable
      17,432 (47.08%) of 37,029 methods reachable
         159 classes,     0 fields, and   538 methods registered for reflection
          59 classes,    59 fields, and    52 methods registered for JNI access
           9 native libraries: dl, g1gc-cr, m, pthread, rt, stdc++, z
    [3/7] Building universe...                                                            (2.1s @ 0.43GB)
    [4/7] Parsing methods...      
  •                                                      (1.8s @ 1.41GB)
    [5/7] Inlining methods...     [***]                                                   (0.8s @ 1.82GB)
    [6/7] Compiling methods...    [******]                                               (35.8s @ 0.58GB)
    [7/7] Creating image...                                                               (3.3s @ 1.16GB)
      10.43MB (50.00%) for code area:     9,222 compilation units
       8.13MB (38.99%) for image heap:  147,772 objects and 5 resources
       2.30MB (11.01%) for other data
      20.86MB in total
    -----------------------------------------------------------------------------------------------------
    Top 10 packages in code area:                     Top 10 object types in image heap:
       1.23MB java.util                                  1.54MB byte[] for code metadata
    983.75KB com.oracle.svm.core.code                 933.82KB java.lang.String
    673.05KB java.text                                912.62KB byte[] for general heap data
    656.40KB java.lang                                871.30KB byte[] for java.lang.String
    647.24KB com.sun.crypto.provider                  584.19KB java.lang.Class
    495.89KB java.util.concurrent                     370.63KB j.u.concurrent.ConcurrentHashMap$Node
    323.36KB sun.security.provider                    352.31KB java.util.HashMap$Node
    300.75KB java.io                                  159.22KB byte[] for reflection metadata
    255.96KB java.math                                153.62KB java.util.HashMap$Node[]
    244.92KB java.util.regex                          148.84KB byte[] for embedded resources
       4.66MB for 145 more packages                      1.66MB for 888 more object types
    -----------------------------------------------------------------------------------------------------
                   1.0s (1.4% of total time) in 34 GCs | Peak RSS: 2.80GB | CPU load: 6.39
    -----------------------------------------------------------------------------------------------------
    Produced artifacts:
    /graalvm-ee-java17-22.3.0/bin/file_provider (executable)
    /graalvm-ee-java17-22.3.0/bin/file_provider.build_artifacts.txt (txt)
    =====================================================================================================
    Finished generating 'file_provider' in 1m 9s.由于在编译时遇到异常会回退到fallback模式,而这个模式下运行编译产物仍需依赖JDK。所以我们使用 -no-fallback 标志,尝试输出纯二进制文件:
    ┌──(root㉿kali)-[/graalvm-ee-java17-22.3.0/bin]
    └─# file file_provider
    file_provider: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b46ce7a88430a706e740b4791d99fa734edbc43b, for GNU/Linux 3.2.0, not stripped可以看到是正常的Linux二进制程序,运行测试:
    ┌──(root㉿kali)-[/graalvm-ee-java17-22.3.0/bin]
    └─# ./file_provider
    Exception in thread "main" java.lang.IllegalStateException: java.util.zip.ZipException: zip END header not found
            at org.springframework.boot.loader.ExecutableArchiveLauncher.<init>(ExecutableArchiveLauncher.java:52)
            at org.springframework.boot.loader.JarLauncher.<init>(JarLauncher.java:48)
            at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
    Caused by: java.util.zip.ZipException: zip END header not found
            at java.base@17.0.5/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1469)
            at java.base@17.0.5/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1477)
            at java.base@17.0.5/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1315)
            at java.base@17.0.5/java.util.zip.ZipFile$Source.get(ZipFile.java:1277)
            at java.base@17.0.5/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:709)
            at java.base@17.0.5/java.util.zip.ZipFile.<init>(ZipFile.java:243)
            at org.springframework.boot.loader.jar.AbstractJarFile.<init>(AbstractJarFile.java:39)
            at org.springframework.boot.loader.jar.JarFile.<init>(JarFile.java:130)
            at org.springframework.boot.loader.archive.JarFileArchive.<init>(JarFileArchive.java:73)
            at org.springframework.boot.loader.archive.JarFileArchive.<init>(JarFileArchive.java:69)
            at org.springframework.boot.loader.Launcher.createArchive(Launcher.java:163)
            at org.springframework.boot.loader.ExecutableArchiveLauncher.<init>(ExecutableArchiveLauncher.java:48)
            ... 2 more报错了。其实并不是代码写错了,更不可能是Spring框架的问题,而是Spring对Native-Image的支持本就处于实验阶段。在最新的SpringBoot3中,需要添加AOT插件,并且添加一系列编译选项才能在Maven中成功编译出可执行镜像。具体怎么做,网上也有不少教程了,可以去试一下,这里偷个懒,就不试了。
    总的来说,如果是原生Java程序,基本上可以做到完美编译。像Spring这类框架用了不少第三方包,又用了很多反射、AOP之类的技术,编译起来出错的概率可能就大一点。不过一些简单的纯Java项目编译二进制后,确实能看到内存消耗和启动速度都有改进,值得尝试。当然,Java生态的主要引领者Spring也在积极拥抱AOT技术,对编译成二进制image的支持很快就会变为正式,在各类后端程序中提供更高的性能、安全性。
    不过话又说回来,GraalVM作为一个商业产品,大规模商用还是免不了要收费和阉割功能的,native-image也有可能受到影响,比如在这里对G1GC的支持,以及未来可能的各种收费功能,只能说Oracle,姜还是老的辣。
    总结

    使用GraalVM编译原生Java程序主要就两步:

    • 用Javac把代码编译成字节码,或者用Maven/Gradle等等打成jar包
    • 用 native-image 命令生成二进制文件
    是不是比把大象装冰箱里还简单?(doge
    好了,今天的内容就到这里。我是ZeroFreeze,一名开发者,未来将持续向大家分享大量Android、Linux相关的技术知识文章。点点关注不迷路,我们下篇文章见~
  • 回复

    举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    快速回复 返回顶部 返回列表