|
众所周知,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相关的技术知识文章。点点关注不迷路,我们下篇文章见~ |
|