本节以实战为主,给大家介绍Java新的编译连接技术,也能体现出Java语言为了适应服务化,本地化的需要,一直在持续改进。
一 Jlink
Jlink工具是在JDK9以后加入的,link是操作系统中用来连接二进制静态文件,产生本地执行文件的工具名称。
JDK提供的工具,取了这样一个名字,估计是有这样的打算吧:通过AOT技术,把Java应用编译成二进制本地文件,可以脱离JRE环境直接执行。
但事实上,目前的jlink工具的作用是:在模块化基础上,可以定制化一个专用的JRE运行环境,而不依赖于JDK环境,适用于云端部署和容器化。
上一篇模块化文章中,我们提到了模块化可以把JDK和应用拆分成一个个模块。Jlink就充分用到了模块化功能。
有一个Java文件StringHash.java,做一些运算操作:
package example;
public final class StringHash {
public static void main(String[] args) {
StringHash sh = new StringHash();
sh.run();
}
void run() {
for (int i=1; i<=20; i++) {
timeHashing(i, 'x');
}
}
void timeHashing(int length, char c) {
final StringBuilder sb = new StringBuilder();
for (int j = 0; j < length * 1_000_000; j++) {
sb.append(c);
}
final String s = sb.toString();
final long now = System.nanoTime();
final int hash = s.hashCode();
final long duration = System.nanoTime() - now;
System.out.println("Length: "+ length +" took: "+ duration +" ns");
}
}
新建一个空的模块化描述文件module-info.java,模块名称叫做example。用JDK11进行编译
/tmp/jlink$ x1/java/jdk11/bin/javac -d mods/ --module-version 1.0 StringHash.java module-info.java
可以看到编译好的应用模块,自动依赖java.base模块
/tmp/jlink$ x1/java/jdk11/bin/javap mods/example/module-info.class
Compiled from "module-info.java"
module example@1.0 {
requires java.base;
}
利用jar工具,将其打包成模块jar文件,并且告知main class类名
/tmp/jlink$ x1/java/jdk11/bin/jar --create --file jar.example-1.0.jar --main-class example.StringHash --module-version 1.0 -C mods .
接下来就可以用jlink生成运行环境了,发在target目录之中
/tmp/jlink$ x1/java/jdk11/bin/jlink --module-path jmods:mods --add-modules example --output target
看一下新的运行环境的java版本,正是java11
/tmp/jlink/target/bin$ ./java --version
openjdk 11 2018-09-25
OpenJDK Runtime Environment 18.9 (build 11+28)
OpenJDK 64-Bit Server VM 18.9 (build 11+28, mixed mode)
在这个独立环境中运行
/tmp/jlink/target/bin$ ./java --module-path example --module example/example.StringHash
Length: 1 took: 3285275 ns
Length: 2 took: 2840250 ns
...
Length: 20 took: 26925550 ns
看看这个环境的目录结构:
/tmp/jlink/target$ tree .
.
├── bin
│ ├── java
│ └── keytool
├── conf
│ ├── net.properties
│ └── security
│ ├── java.policy
│ ├── java.security
│ └── policy
│ ├── limited
│ │ ├── default_local.policy
...
├── include
│ ├── classfile_constants.h
│ ├── jni.h
│ ├── jvmticmlr.h
│ ├── jvmti.h
│ └── linux
│ └── jni_md.h
├── legal
│ └── java.base
│ ├── ADDITIONAL_LICENSE_INFO
│ ├── aes.md
...
├── lib
│ ├── classlist
│ ├── jexec
│ ├── jli
│ │ └── libjli.so
│ ├── jrt-fs.jar
│ ├── jvm.cfg
│ ├── libjava.so
│ ├── libjimage.so
│ ├── libjsig.so
│ ├── libnet.so
│ ├── libnio.so
│ ├── libverify.so
│ ├── libzip.so
│ ├── modules
│ ├── security
│ │ ├── blacklisted.certs
│ │ ├── cacerts
│ │ ├── default.policy
│ │ └── public_suffix_list.dat
│ ├── server
│ │ ├── libjsig.so
│ │ ├── libjvm.so
│ │ └── Xusage.txt
│ └── tzdb.dat
└── release
14 directories, 48 files
这个“JRE”只有48M大小,比标准JDK少了很多,而且include等很多文件也可以去除。
这个运行环境可以整个包拷贝到其他的同类型机器上,不需要安装Java环境,就可以直接运行,非常适合云端容器化部署。
不过这个jlink出来的环境依然是Java,就是利用模块化能力,把其他没有用到的模块全部从JDK移除,得到的一个最小化的JRE运行环境。
我们看生成的JRE和官方下载的JDK,在lib目录下都有一个modules文件,这个就是JIMAGE映像文件,里面包含了所需要的模块。
可以使用jimage工具来查看。
/tmp/jlink$ x1/java/jdk11/bin/jimage info target/lib/modules
Major Version: 1
Minor Version: 0
Flags: 0
Resource Count: 6538
Table Length: 6538
Offsets Size: 26152
Redirects Size: 26152
Locations Size: 124679
Strings Size: 151391
Index Size: 328402
/tmp/jlink$ x1/java/jdk11/bin/jimage list target/lib/modules | grep example
Module: example
example/StringHash.class
能够看到StringHash类被包含在映像文件中
二 GraalVM
上述的JLink始终还是对JDK的裁剪,而GraalVM却是采用另外的思路。
OpenJDK采用的Hotpot编译器,其中包含两个独立的JIT(即时编译)编译器,C1和C2,分别用于客户端编译和服务端编译。
Java在运行时有即时编译操作,虚拟机会自动使用最合适的编译器开启编译过程
我们还是使用上面的代码范例,先编译好,然后运行,同时使用参数PrintCompilation打印即时编译过程。
/tmp/jlink$ x1/java/jdk11/bin/java -XX:+PrintCompilation example.StringHash
62 1 3 java.lang.Object::<init> (1 bytes)
62 2 3 java.lang.StringLatin1::hashCode (42 bytes)
63 3 3 java.lang.String::isLatin1 (19 bytes)
63 4 3 java.lang.String::hashCode (49 bytes)
...
Length: 19 took: 30008923 ns
1486 263 1 java.nio.Buffer::limit (5 bytes)
Length: 20 took: 31131184 ns
可以看到编译过程和执行过程的关系。对于很多只执行一次的代码,无需进行太高等级的编译优化。而对于多次调用的代码,JDK会使用C2重新编译这部分代码,从而实现更优的效率。
Java可以生成高效执行的代码,功劳就是C2编译器。
除了运行时编译优化之外,另一个思路是AOT(Ahead of Time),提前编译,也就是和C语言的程序一样,先针对目标机器编译好,直接运行就可以了。
GraalVM就是这样的思路,它同时可以支持即时编译和AOT方式。

我们先看看它是怎么进行即时编译的。Graal接管了C2编译过程,需要通过设置UnlockExperimentalVMOptions,EnableJVMCI,UseJVMCICompiler等参数:
/tmp/jlink$ x1/java/jdk11/bin/java -XX:+PrintCompilation -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler example.StringHash
76 1 3 java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
77 2 3 jdk.internal.misc.Unsafe::getObjectAcquire (7 bytes)
79 3 3 java.lang.Object::<init> (1 bytes)
79 4 3 java.lang.StringLatin1::hashCode (42 bytes)
...
4502 17 3 java.util.Objects::requireNonNull (14 bytes) made not entrant
4502 1131 4 java.nio.Buffer::limit (74 bytes)
Length: 20 took: 34624394 ns
4823 3493 3 java.util.Collections::unmodifiableList (27 bytes)
可以看到打印出来的compilation信息非常的多。
我们分别看一下使用Graal和不使用的执行效率
/tmp/jlink$ x1/java/jdk11/bin/java example.StringHash
Length: 1 took: 1345748 ns
Length: 2 took: 2921367 ns
Length: 3 took: 4228897 ns
Length: 4 took: 5974513 ns
...
Length: 20 took: 28209226 ns
/tmp/jlink$ x1/java/jdk11/bin/java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler StringHash
Length: 1 took: 4996295 ns
Length: 2 took: 7437932 ns
Length: 3 took: 14184485 ns
Length: 4 took: 15232377 ns
...
Length: 20 took: 33059720 ns
咦,似乎速度差不多,普通模式还快一些。
可能是这个例子偏计算类型,Graal在其他一些方面,运行优势还是很大的。
下一步我们将其转换成Native本地二进制运行文件。
因为目前下载回来的GraalVM CE版1.0.0-rc6,是基于Java8开发的,所以需要用JDK8重新编译一边java文件,同时不能有module-info文件。
/tmp/jlink$ javac -d graal/ StringHash.java
利用新版jar打一个包
/tmp/jlink$ x1/java/jdk11/bin/jar --create --file lib/example-1.0-jdk8.jar --main-class example.StringHash -C graal/ .
使用GraalVM中提供的native-image来生成二进制包
/tmp/jlink$ tmp/graalvm-ce-1.0.0-rc6/bin/native-image -jar lib/example-1.0-jdk8.jar Build on Server(pid: 22244, port: 38252)
[example-1.0-jdk8:22244] classlist: 706.85 ms
[example-1.0-jdk8:22244] (cap): 3,326.09 ms
[example-1.0-jdk8:22244] setup: 6,026.98 ms
[example-1.0-jdk8:22244] (typeflow): 38,239.89 ms
[example-1.0-jdk8:22244] (objects): 9,578.26 ms
[example-1.0-jdk8:22244] (features): 1,110.93 ms
[example-1.0-jdk8:22244] analysis: 50,788.46 ms
[example-1.0-jdk8:22244] universe: 1,697.45 ms
[example-1.0-jdk8:22244] (parse): 47,747.31 ms
[example-1.0-jdk8:22244] (inline): 1,152.30 ms
[example-1.0-jdk8:22244] (compile): 5,754.09 ms
[example-1.0-jdk8:22244] compile: 55,444.81 ms
[example-1.0-jdk8:22244] image: 1,372.28 ms
[example-1.0-jdk8:22244] write: 3,298.02 ms
[example-1.0-jdk8:22244] [total]: 120,977.76 ms
$ ls -l
total 5264
查看一下生成文件大小为5M多
-rwxrwxr-x 1 shihang shihang 5351481 Oct 3 11:41 example-1.0-jdk8
执行结果和Java版本完全一样
/tmp/jlink$ ./example-1.0-jdk8
Length: 1 took: 3763924 ns
Length: 2 took: 4476763 ns
Length: 3 took: 6479676 ns
Length: 4 took: 7847759 ns
Length: 5 took: 9898429 ns
...
Length: 19 took: 37304258 ns
Length: 20 took: 39175620 ns
使用ldd工具查看依赖的动态库
/tmp/jlink$ ldd example-1.0-jdk8
linux-vdso.so.1 => (0x00007fff5f9e2000)
libpthread.so.0 => lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f5ae9bb7000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f5ae99af000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5ae95e9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5ae9df5000)
可以看到完全脱离了Java运行环境,在Linux操作系统上就可以运行。
这样就更加适应云计算和容器环境运行了。
不过我们再看一下运行时间,似乎比前一步中Java运行耗时还长。
这个可能还是因为计算密集型。
另外一个例子,本地二进制文件运行速度就比Java运行速度快的多。
/tmp/native-image-service-loader-demo-master$ time java -jar target/ServiceLoaderTest-1.0-SNAPSHOT.jar
services.iterator().hasNext() = true
service implementation = class service.ServiceImplementation0
service implementation = class service.ServiceImplementation1
real 0m0.124s
user 0m0.118s
sys 0m0.024s
/tmp/native-image-service-loader-demo-master$ time ./ServiceLoaderTest-1.0-SNAPSHOT
services.iterator().hasNext() = true
service implementation = class service.ServiceImplementation0
service implementation = class service.ServiceImplementation1
real 0m0.003s
user 0m0.000s
sys 0m0.003s
前一个是Java运行时间,后一个是native化的运行时间。可以看到速度优化在十倍以上。
我觉得GraalVM还处在一个逐步完善的期间,对于复杂的特别是涉及到很多反射调用的Java应用,GraalVM还是无法顺畅的本地化。
但这个方向是非常正确的,可以通过编程时aot友好化和逐步改进,未来对Java应用有一个良好的支持,则Java将继续会成为云计算主流开发语言。
同时Graal的目标是一个平台,除了Java,还会支持多种语言,JavaScript,Node.JS,Ruby、Python、R语言和LLVM字节码等。
整个平台由三大组件构成:
Graal:一个由Java语言编写的JIT编译器。
SubstrateVM:一个对执行容器抽象层的轻量级封装。
Truffle:一个用于构建语言解析器的工具集和API。
体系架构图

多种语言之间可以无缝的互相调用。
我个人还是最关注Graal的本地化能力和即时编译优化能力。而且希望这两个功能最终可以自由使用,而不必需要商业授权许可。




