写点什么

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

  • 2018-04-11
  • 本文字数:4027 字

    阅读完需:约 13 分钟

引言

对于大部分应用开发者来说,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-04-11 17:4420498

评论

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

第十周学习总结

饭桶

Mac下Docker Desktop配置阿里云镜像加速器

jiangling500

Docker 阿里云镜像加速器

揭秘11.11监控排障利器 京东高稳定日志服务深度解析

京东科技开发者

云计算 DevOps 日志监控

C++语言中std::array的神奇用法总结,你需要知道!

华为云开发者联盟

容器 数组 函数

Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)

YourBatman

云原生 Spring Boot 新特性

华为云MVP高浩:打破AI开发瓶颈,解决数据、算法、算力三大难题

华为云开发者联盟

人工智能 数据 华为云

最近我发现瑞幸在这样做私域运营

Linkflow

营销数字化 客户数据平台 CDP 私域运营

薇娅携手中国航天基金会与我们的太空 带你“益起探月,共舞九天“

什么是低代码(Low-Code)?

阿里巴巴云原生

程序员 云原生 代码

Linux 笔记(三): 软件安装

Leo

Linux 学习 大前端

深度剖析,为何C语言在开发领域的地位如此稳固

Philips

Python .net rust C语言 Go 语言

一点就透的二分查找算法

比伯

Java 编程 程序员 面试 计算机

第十周课后练习

饭桶

Mysql数据备份与恢复

张攀钦

MySQL

《我想进大厂》之Spring夺命连环10问

艾小仙

Java spring 程序员 面试 大厂

架构师第一期作业(第 11 周)

Cheer

作业

产业新基建,撬动数字经济发展新机遇

京东科技开发者

人工智能 新基建 京东

源码 | 浅谈Webpack原理,以及loader和plugin实现。

梁龙先森

大前端 webpack

CAP理论

DL

不懂源码?来看看阿里P8亲自手码的Spring源码解析整套笔记,高薪offer唾手可得!

比伯

Java 编程 架构 面试 计算机

Kubernetes初体验--用Kubernetes部署一个Web服务

网管

Kubernetes k8s Web 服务 Go 语言

使用 Jira Service Management 管理资产,您需要知道的5件事

Atlassian

数字化转型 Atlassian Jira ITSM ITIL

接口测试如何在json中引用mock变量

测试人生路

json 接口测试 Mock

架构师训练营第十周作业

文智

极客大学架构师训练营

什么是物联网?常见IoT 物联网协议最全讲解

华章IT

物联网 IoT

“摸爬滚打”多年,从月薪3K到30Kjava大神,我是怎么蜕变的?

比伯

Java 编程 架构 面试 计算机

使用resilio实现多集群的k8s pod数据双向非实时同步

东风微鸣

Kubernetes 探索与实践 openshift

Canal 组件简介与 vivo 帐号实践

vivo互联网技术

数据库 分布式 数据存储

「面试必备」最新整理出的腾讯C++后台开发面试笔记

linux大本营

c++ Linux 后台开发 架构师

一线大厂欺负程序员?京东单方面辞退38岁P7员工三次败诉

Java架构师迁哥

排查指南 | mPaaS 小程序提示“网络不给力”时该如何排查?

蚂蚁集团移动开发平台 mPaaS

小程序 网络 小程序生态 mPaaS

深入浅出Java 10的实验性JIT编译器Graal_语言 & 开发_郑雨迪_InfoQ精选文章