业务云原生架构、推荐系统以及线上生活等热点方向的高可用高性能业务架构有哪些?点击了解 了解详情
写点什么

深入浅出 Java 10 的实验性 JIT 编译器 Graal

2018 年 4 月 11 日

引言

对于大部分应用开发者来说,Java 编译器指的是 JDK 自带的 javac 指令。这一指令可将 Java 源程序编译成.class 文件,其中包含的代码格式我们称之为 Java bytecode(Java 字节码)。这种代码格式无法直接运行,但可以被不同平台 JVM 中的 interpreter 解释执行。由于 interpreter 效率低下,JVM 中的 JIT compiler(即时编译器)会在运行时有选择性地将运行次数较多的方法编译成二进制代码,直接运行在底层硬件上。Oracle 的 HotSpot VM 便附带两个用 C++ 实现的 JIT compiler:C1 及 C2。

与 interpreter,GC 等 JVM 的其他子系统相比,JIT compiler 并不依赖于诸如直接内存访问的底层语言特性。它可以看成一个输入 Java bytecode 输出二进制码的黑盒,其实现方式取决于开发者对开发效率,可维护性等的要求。Graal 是一个以 Java 为主要编程语言,面向 Java bytecode 的编译器。与用 C++ 实现的 C1 及 C2 相比,它的模块化更加明显,也更加容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;亦可以作为静态编译器,实现 AOT 编译。在 Java 10 中,Graal 作为试验性 JIT compiler 一同发布( JEP 317 )。这篇文章将介绍 Graal 在动态编译上的应用。有关静态编译,可查阅 JEP 295 Substrate VM

分层编译(tiered compilation

在介绍 Graal 前,我们先了解 HotSpot 中的 tiered compilation。前面提到,HotSpot 集成了两个 JIT compiler — C1 及 C2(或称为 Client 及 Server)。两者的区别在于,前者没有应用激进的优化技术,因为这些优化往往伴随着耗时较长的代码分析。因此,C1 的编译速度较快,而 C2 所编译的方法运行速度较快。在 Java 7 前,用户需根据自己的应用场景选择合适的 JIT compiler。举例来说,针对偏好高启动性能的 GUI 用户端程序则使用 C1,针对偏好高峰值性能的服务器端程序则使用 C2。

Java 7 引入了 tiered compilation 的概念,综合了 C1 的高启动性能及 C2 的高峰值性能。这两个 JIT compiler 以及 interpreter 将 HotSpot 的执行方式划分为五个级别:

  • level 0:interpreter 解释执行
  • level 1:C1 编译,无 profiling
  • level 2:C1 编译,仅方法及循环 back-edge 执行次数的 profiling
  • level 3:C1 编译,除 level 2 中的 profiling 外还包括 branch(针对分支跳转字节码)及 receiver type(针对成员方法调用或类检测,如 checkcast,instnaceof,aastore 字节码)的 profiling
  • level 4:C2 编译

其中,1 级和 4 级为接受状态 — 除非已编译的方法被 invalidated(通常在 deoptimization 中触发),否则 HotSpot 不会再发出该方法的编译请求。

上图列举了4 种编译模式(非全部)。通常情况下,一个方法先被解释执行(level 0),然后被C1 编译(level 3),再然后被得到profile 数据的C2 编译(level 4)。如果编译对象非常简单,虚拟机认为通过C1 编译或通过C2 编译并无区别,便会直接由C1 编译且不插入profiling 代码(level 1)。在C1 忙碌的情况下,interpreter 会触发profiling,而后方法会直接被C2 编译;在C2 忙碌的情况下,方法则会先由C1 编译并保持较少的profiling(level 2),以获取较高的执行效率(与3 级相比高30%)。

Graal 可替换 C2 成为 HotSpot 的顶层 JIT compiler,即上述 level 4。与 C2 相比,Graal 采用更加激进的优化方式,因此当程序达到稳定状态后,其执行效率(峰值性能)将更有优势。

早期的 Graal 同 C1 及 C2 一样,与 HotSpot 是紧耦合的。这意味着每次编译 Graal 均需重新编译 HotSpot。 JEP 243 将 Graal 中依赖于 HotSpot 的代码分离出来,形成 Java-Level JVM Compiler Interface(JVMCI)。该接口主要提供如下三种功能:

  • 响应 HotSpot 的编译请求,并分发给 Java-Level JIT compiler
  • 允许 Java-Level JIT compiler 访问 HotSpot 中与 JIT compilation 相关的数据结构,包括类,字段,方法及其 profiling 数据等,并提供这些数据结构在 Java 层面的抽象
  • 提供 HotSpot codecache 的 Java 抽象,允许 Java-Level JIT compiler 部署编译完成的二进制代码

综合利用这三种功能,我们可以将 Java-Level 编译器(不局限于 Graal)集成至 HotSpot 中,响应 HotSpot 发出的 level 4 的编译请求并将编译后的二进制代码部署到 HotSpot 的 codecache 中。此外,单独利用上述第三种功能可以绕开 HotSpot 的编译系统 — Java-Level 编译器将作为上层应用的类库直接部署编译后的二进制代码。Graal 自身的单元测试便是依赖于直接部署而非等待 HotSpot 发出编译请求; Truffle 亦是通过此机制部署编译后的语言解释器。

Graal v.s. C2

前面提到,JIT Compiler 并不依赖于底层语言特性,它仅仅是一种代码形式到另一种代码形式的转换。因此,理论上任意 C2 中以 C++ 实现的优化均可以在 Graal 中通过 Java 实现,反之亦然。事实上,许多 C2 中实现的优化均被移植到 Graal 中,如近期由其他开发者贡献的 String.compareTointrinsic 的移植。当然,局限于 C++ 的开发 / 维护难度(个人猜测),许多 Graal 中被证明有效的优化并没有被成功移植到 C2 上,这其中就包含 Graal 的 inlining 算法及 partial escape analysis(PEA)。

Inlining 是指在编译时识别 callsite 的目标方法,将其方法体纳入编译范围并用其返回结果替换原 callsite。最简单直观的例子便是 Java 中常见的 getter/setter 方法 — inlining 可以将一个方法中调用 getter/setter 的 callsite 优化成单一内存访问指令。Inlining 被业内戏称为优化之母,其原因在于它能引发更多优化。然而在实践中我们往往受制于编译单元大小或编译时间的限制,无法无限制地递归 inline。因此,inlining 的算法及策略很大程度上决定了编译器的优劣,尤其是在使用 Java 8 的 stream API 或使用 Scala 语言的场景下。这两种场景对应的 Java bytecode 包含大量的多层单方法调用。

Graal 拥有两个 inliner 实现。社区版的 inliner 采用的是深度优先的搜索方式,在分析某一方法时,一旦遇到不值得 inline 的 callsite 时便回溯至该方法的调用者。Graal 允许自定义策略以判断某一 callsite 值不值得 inline。默认情况下,Graal 会采取一种相对贪婪的策略,根据 callsite 的目标方法的大小做出相应的决定。Graal enterprise 的 inliner 则对所有 callsite 进行加权排序,其加权算法取决于目标方法的大小以及可能引发的优化。当目标方法被 inline 后,其包含的 callsite 同样会进入该加权队列中。这两种搜索方式都较为适合拥有多层单方法调用的应用场景。

Escape analysis(逃逸分析,EA)是一类识别对象动态范围的程序分析。编译器中常见的应用有两类:如果对象仅被单一线程访问,则可去除针对该对象的锁操作;如果对象为堆分配且仅被单一方法访问(inlining 的重要性再次体现),则可将该对象转化成栈分配。后者通常伴随着 scalar replacement,即将对对象字段的访问替换成对虚拟局部操作数的访问,从而进一步将对象由栈分配转换成虚拟分配。这不仅节省了原本用于存放对象 header 的内存空间,而且可以在 register allocator 的帮助下将(部分)对象字段存放在寄存器中,在节省内存的同时提高执行效率(内存访问转换成寄存器访问)。

Java 中常见的 for-each loop 是 EA 的一大目标客户。我们知道 for-each loop 会调用被遍历对象的 iterator 方法,返回一个实现 interface Iterator 的对象,并利用其 hasNext 及 next 接口进行遍历。Java collections 中的容器类(如 ArrayList)通常会构造一个新的 Iterator 实例,其生命周期局限于该 for-each loop 中。如若 Iterator 实例的构造函数以及 hasNext,next 方法调用(连同它们方法体中以 this 为 receiver 的方法调用,如 checkForComodification())都被 inline,EA 会认为该实例没有逃逸,并采取栈分配及 scalar replacement。

理想情况下,Foo.bar 会被优化成如下代码:

HotSpot 的 C2 便已应用控制流无关的 EA 实现 scalar replacement。而 Graal 的 PEA 则在此基础上引入了控制流信息,将所有的堆分配操作虚拟化,并仅在对象确定逃逸的分支 materialize。与 C2 的 EA 相比,PEA 分析效率较低,但能够在对象没有逃逸的分支上实现 scalar replacement。如下例所示,如果 then-branch 的执行概率为 1%,那么被 PEA 优化后的代码在 99% 的情况下并不会执行堆分配,而 C2 的 EA 则 100% 会执行堆分配。另一个典型的例子是渲染引擎 Sunflow — 在运行 DaCapo benchmark suite 所附带的默认 workload 时,Graal 的 PEA 判定约 27% 的堆分配(共占 700M)可被虚拟化。该数字远超 C2 的 EA。

使用 Graal

在Java 10 (Linux/x64, macOS/x64) 中,默认情况下HotSpot 仍使用C2,但通过向java 命令添加-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 参数便可将C2 替换成Graal。

Oracle Labs GraalVM 是由 Oracle Labs 直接发布的 JDK 版本。它基于 Java 8,并且囊括了 Graal enterprise。如果对源代码感兴趣,可直接签出 Graal 社区版的 GitHub repo 。源代码的编译需借助 mx 工具及 labsjdk (注:请下载页面最下方的 labsjdk,直接使用 GraalVM 可能会导致编译问题)。

在 graal/compiler 目录下使用 mx eclipseinit,mx intellijinit 或 mx netbeansinit 可分别生成 Eclipse,IntelliJ 或 NetBeans 的工程配置文件。

参考链

Upcoming: advanced topics in Graal compiler

  • Debugging compiled code
  • Deoptimization & Java-level assumption — SpeculationLog
  • Graal method substitution & Snippet
  • Implementing JVM intrinsics

作者介绍:郑雨迪,现于 Oracle Labs 任职高级研究员,是 Graal 编译器组的核心开发者之一。他的研究方向包括动态编译及程序分析。在加入 Oracle Labs 前,郑雨迪于瑞士卢加诺大学攻读并获得博士学位。他即将在 QCon 北京 2018 现场分享《GraalVM 及其生态系统》,敬请关注。

2018 年 4 月 11 日 17:4417451

评论

发布
暂无评论
发现更多内容

阿里专家终于把“大数据+Netty+分布式实战”彻底讲明白了

周老师

Java 编程 程序员 面试 面试架构

网络安全入门之owasp top10漏洞详解

Machine Gun

网络安全 信息安全 渗透测试 WEB安全

Java程序员如何在“黄金五年”实现最大价值?

学Java关注我

Java 编程 架构 互联网 计算机

Yii2反序列化RCE 新POP链

Thrash

☕【Java技术之旅】如何彻底认识AQS的原理(上篇)

李浩宇/Alex

Java AQS JVM JUC 5月日更

云原生下的灰度体系建设

阿里巴巴云原生

容器 运维 云原生 k8s 监控

Nginx如何配置Http、Https、WS、WSS?

冰河

nginx 负载均衡 反向代理 https HTTP

从SPACE矩阵,看5G究竟是否在走向成功?

脑极体

阿里最新春招面经,腾讯/美团/字节1万道Java中高级面试题

周老师

Java 编程 架构 架构程序员 编程面试

解除限制!阿里,快手,美团等一线大厂Java内部面试资料开放下载

马小晴

Java 程序员 面试 架构师 Java 25 周年

机器学习 Machine Learning- 吴恩达Andrew Ng 第5~15课总结 John 易筋 ARTS 打卡 Week 47

John(易筋)

ARTS 打卡计划

太赞了!美团大牛强推的Spring事务笔记,上线仅1天就获赞上万

飞飞JAva

Java 事务spring

王兴的失败观

池建强

成功 王兴 创业失败启示录

一千座5G工厂的花苞

脑极体

边缘计算与云计算的故事

攻城先森

云计算 边缘计算 5月日更

如何判断企业赚不赚钱?

石云升

创业 财务分析 5月日更

云原生的进一步具象化

阿里巴巴云原生

大数据 容器 云原生 监控 中间件

实战排查由于系统负载引起的服务响应异常

Coder的技术之路

高并发 性能调优 线上问题

HTB:Jerry渗透测试

Machine Gun

网络安全 信息安全 渗透测试 WEB安全

GreenPlum中的资源队列

数据社

greenplum 5月日更

你敢信?1年半经验,2本学历,Curd背景,美团四面,竟给30K

神奇小汤圆

Java 程序员 架构 面试

SSL / TLS协议解析!什么是SNI? SNI 识别?

明儿

数据工作者必备工作技能:数据治理

博文视点Broadview

异步编程的几种方式,你知道几种?

xcbeyond

Java 异步编程 5月日更

太好用了!阿里大佬离职带出的“高并发系统设计”学习笔记,成功帮我斩获3个大厂Offer

神奇小汤圆

Java 程序员 架构 计算机

重磅!数字人民币接入支付宝!

CECBC区块链专委会

数字人民币

差点扛不住了,阿里巴巴支付宝面试 5 轮暴击,终获 Offer

Java架构师迁哥

工业制造业亟需数字化转型,区块链可以发挥哪些价值?

CECBC区块链专委会

区块链

强!上线3天获10w浏览量,京东T8纯手码Redis缓存手册,我粉了

飞飞JAva

redis

网络攻防学习笔记 Day10

穿过生命散发芬芳

5月日更 网络攻防

常见流媒体服务器方案对比分析

liuzhen007

音视频 5月日更

深入浅出Java 10的实验性JIT编译器Graal-InfoQ