搜文章
推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > 开源中间件 > Java新优特性之Jlink和GraalVM

Java新优特性之Jlink和GraalVM

开源中间件 2018-10-29

本节以实战为主,给大家介绍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的本地化能力和即时编译优化能力。而且希望这两个功能最终可以自由使用,而不必需要商业授权许可。


版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《Java新优特性之Jlink和GraalVM》的版权归原作者「开源中间件」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注开源中间件微信公众号

开源中间件微信公众号:useopen

开源中间件

手机扫描上方二维码即可关注开源中间件微信公众号

开源中间件最新文章

精品公众号随机推荐