深入理解Java虚拟机笔记1——概述
文章目录
展望Java技术的未来
今天的Java正处于机遇与挑战并存的时期,Java未来能否继续壮大发展,某种程度上取决于如何应对当下已出现的挑战,本文将按照这个脉络来组织,介绍现在仍处于Oracle Labs中的Graal VM、Valhalla、Amber、Loom、Panama等面向未来的研究项目。
无语言倾向
Oracle Labs在2018年新公开了一项黑科技:Graal VM。Graal VM被官方称为“Universal VM”和“Polyglot VM”,这是一个在HotSpot虚拟机基础上增强而成 的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用,这里“任何语言”包括了Java、Scala、Groovy、Kotlin等基于Java虚拟机之上的语言,还包括了C、C++、Rust等基于LLVM的语言,同时支持其他像JavaScript、Ruby、Python和R语言等。Graal VM可以无额外开销地混合使用这些编程语言,支持不同语言中混用对方的接口和对象,也能够支持这些语言使用已经编写好的本地库文件。
Graal VM的基本工作原理是将这些语言的源代码(例如JavaScript)或源代码编译后的中间格式(例如LLVM字节码)通过解释器转换为能被Graal VM接受的中间表示(Intermediate Representation,IR),譬如设计一个解释器专门对LLVM输出的字节码进行转换来支持C和C++语言,这个过程称为程序特化(Specialized,也常被称为Partial Evaluation)。Graal VM提供了Truffle工具集来快速构建面向一种新语言的解释器,并用它构建了一个称为Sulong的高性能LLVM字节码解释器。
从更严格的角度来看,Graal VM才是真正意义上与物理计算机相对应的高级语言虚拟机,理由是它与物理硬件的指令集一样,做到了只与机器特性相关而不与某种高级语言特性相关。
对Java而言,Graal VM本来就是在HotSpot基础上诞生的,天生就可作为一套完整的符合Java SE 8标准的Java虚拟机来使用。它和标准的HotSpot的差异主要在即时编译器上,其执行效率、编译质量目前与标准版的HotSpot相比也是互有胜负。但现在Oracle Labs和美国大学里面的研究院所做的最新即时编译技术的研究全部都迁移至基于Graal VM之上进行了,其发展潜力令人期待。
新一代即时编译器
对需要长时间运行的应用来说,由于经过充分预热,热点代码会被HotSpot的探测机制准确定位捕获,并将其编译为物理硬件可直接执行的机器码,在这类应用中Java的运行效率很大程度上取决于即时编译器所输出的代码质量。
HotSpot虚拟机中含有两个即时编译器,分别是编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)以及编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2),通常它们会在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统。
自JDK 10起,HotSpot中又加入了一个全新的即时编译器:Graal编译器,看名字就可以联想到它是来自于前一节提到的Graal VM。Graal编译器是以C2编译器替代者的身份登场的。Graal能够做比C2更加复杂的优化,如“部分逃逸分析”(Partial Escape Analysis),也拥有比C2更容易使用激进预测性优化(Aggressive Speculative Optimization)的策略,支持自定义的预测性假设等。
Graal编译器还未经过足够多的实践验证,所以仍然带着“实验状态”的标签,需要用开关参数去激活:
|
|
向Native迈进
对不需要长时间运行的,或者小型化的应用而言,Java(而不是指Java ME)天生就带有一些劣势。
在微服务架构的视角下,应用拆分后,单个微服务很可能就不再需要面对数十、数百GB乃至TB的内存,有了高可用的服务集群,也无须追求单个服务要7×24小时不间断地运行,它们随时可以中断和更新;但相应地,Java的启动时间相对较长,需要预热才能达到最高性能等特点就显得相悖于这样的应用场景。在无服务架构中,矛盾则可能会更加突出。
Java在最新的几个JDK版本的功能清单中,已经陆续推出了跨进程的、可以面向用户程序的类型信息共享(Application Class Data Sharing,AppCDS,允许把加载解析后的类型信息缓存起来,从而提升下次启动速度,原本CDS只支持Java标准库,在JDK 10时的AppCDS开始支持用户的程序代码)、无操作的垃圾收集器(Epsilon,只做内存分配而不做回收的收集器,对于运行完就退出的应用十分合适)等改善措施。而酝酿中的一个更彻底的解决方案,是逐步开始对提前编译(Ahead of Time Compilation,AOT)提供支持。
提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以减少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢”的不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。但是提前编译的坏处也很明显,它破坏了Java“一次编写,到处运行”的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定。
早在JDK 9时期,Java就提供了实验性的Jaotc命令来进行提前编译,不过多数人试用过后都颇感失望,大家原本期望的是类似于Excelsior JET那样的编译过后能生成本地代码完全脱离Java虚拟机运行的解决方案,但Jaotc其实仅仅是代替即时编译的一部分作用而已,仍需要运行于HotSpot之上。
直到Substrate VM出现,才算是满足了人们心中对Java提前编译的全部期待。Substrate VM是在Graal VM 0.20版本里新出现的一个极小型的运行时环境,包括了独立的异常处理、同步调度、线程管理、内存管理(垃圾收集)和JNI访问等组件,目标是代替HotSpot用来支持提前编译后的程序执行。它还包含了一个本地镜像的构造器(Native Image Generator),用于为用户程序建立基于Substrate VM的本地运行时镜像。这个构造器采用指针分析(Points-To Analysis)技术,从用户提供的程序入口出发,搜索所有可达的代码。在搜索的同时,它还将执行初始化代码,并在最终生成可执行文件时,将已初始化的堆保存至一个堆快照之中。这样一来,Substrate VM就可以直接从目标程序开始运行,而无须重复进行Java虚拟机的初始化过程。但相应地,原理上也决定了Substrate VM必须要求目标程序是完全封闭的,即不能动态加载其他编译器不可知的代码和类库。基于这个假设,Substrate VM才能探索整个编译空间,并通过静态分析推算出所有虚方法调用的目标方法。
Substrate VM补全了Graal VM“Run Programs Faster Anywhere”愿景蓝图里的最后一块拼图,让Graal VM支持其他语言时不会有重量级的运行负担。譬如运行JavaScript代码,Node.js的V8引擎执行效率非常高,但即使是最简单的HelloWorld,它也要使用约20MB的内存,而运行在Substrate VM上的Graal.js,跑一个HelloWorld则只需要4.2MB内存,且运行速度与V8持平。Substrate VM的轻量特性,使得它十分适合嵌入其他系统,譬如Oracle自家的数据库就已经开始使用这种方式支持用不同的语言代替PL/SQL来编写存储过程。
灵活的胖子
模块化方面原本是HotSpot的弱项,监控、执行、编译、内存管理等多个子系统的代码相互纠缠。而IBM的J9就一直做得就非常好,面向Java ME的J9虚拟机与面向Java EE的J9虚拟机可以是完全由同一套代码库编译出来的产品,只有编译时选择的模块配置有所差别。
现在,HotSpot虚拟机也有了与J9类似的能力,能够在编译时指定一系列特性开关,让编译输出的HotSpot虚拟机可以裁剪成不同的功能,譬如支持哪些编译器,支持哪些收集器,是否支持JFR、AOT、CDS、NMT等都可以选择。能够实现这些功能特性的组合拆分,反映到源代码不仅仅是条件编译,更关键的是接口与实现的分离。
早期(JDK 1.4时代及之前)的HotSpot虚拟机为了提供监控、调试等不会在《Java虚拟机规范》中约定的内部功能和数据,就曾开放过Java虚拟机信息监控接口(Java Virtual Machine Profiler Interface,JVMPI)与Java虚拟机调试接口(Java Virtual Machine Debug Interface,JVMDI)供运维和性能监控、IDE等外部工具使用。到了JDK 5时期,又抽象出了层次更高的Java虚拟机工具接口(Java Virtual Machine Tool Interface,JVMTI)来为所有Java虚拟机相关的工具提供本地编程接口集合,到JDK 6时JVMTI就完全整合代替了JVMPI和JVMDI的作用。
在JDK 9时期,HotSpot虚拟机开放了Java语言级别的编译器接口(Java Virtual Machine CompilerInterface,JVMCI),使得在Java虚拟机外部增加、替换即时编译器成为可能,这个改进实现起来并不费劲,但比起之前JVMPI、JVMDI和JVMTI却是更深层次的开放,它为不侵入HotSpot代码而增加或修改HotSpot虚拟机的固有功能逻辑提供了可行性。Graal编译器就是通过这个接口植入到HotSpot之中。
到了JDK 10,HotSpot又重构了Java虚拟机的垃圾收集器接口(Java Virtual Machine Compiler Interface),统一了其内部各款垃圾收集器的公共行为。有了这个接口,才可能存在日后(今天尚未)某个版本中的CMS收集器退役,和JDK 12中Shenandoah这样由Oracle以外其他厂商领导开发的垃圾收集器进入HotSpot中的事情。如果未来这个接口完全开放的话,甚至有可能会出现其他独立于HotSpot的垃圾收集器实现。
语言语法持续增强
JDK 7的Coins项目结束以后,Java社区又创建了另外一个新的语言特性改进项目Amber,JDK10至13里面提供的新语法改进基本都来自于这个项目,譬如:
- JEP 286:Local-Variable Type Inference,在JDK 10中提供,本地类型变量推断。
- JEP 323:Local-Variable Syntax for Lambda Parameters,在JDK 11中提供,JEP 286的加强,使它可以用在Lambda中。
- JEP 325:Switch Expressions,在JDK 13中提供,实现switch语句的表达式支持。
- JEP 335:Text Blocks,在JDK 13中提供,支持文本块功能,可以节省拼接HTML、SQL等场景里大量的“+”操作。
还有一些是仍然处于草稿状态或者暂未列入发布范围的JEP,可供窥探未来Java语法的变化,譬如:
- JEP 301:Enhanced Enums,允许常量类绑定数据类型,携带额外的信息。
- JEP 302:Lambda Leftovers,用下划线来表示Lambda中的匿名参数。
- JEP 305:Pattern Matching for instanceof,用instanceof判断过的类型,在条件分支里面可以不需要做强类型转换就能直接使用。
除语法糖以外,语言的功能也在持续改进之中,以下几个项目是目前比较明确的,也是受到较多关注的功能改进计划:
-
Project Loom
现在的Java做并发处理的最小调度单位是线程,Java线程的调度是直接由操作系统内核提供的(这方面的内容可见本书第12章),会有核心态、用户态的切换开销。而很多其他语言都提供了更加轻量级的、由软件自身进行调度的用户线程(曾经非常早期的Java也有绿色线程),譬如Golang的Groutine、D语言的Fiber等。Loom项目就准备提供一套与目前Thread类API非常接近的Fiber实现。
-
Project Valhalla
提供值类型和基本类型的泛型支持,并提供明确的不可变类型和非引用类型的声明。值类型的作用和价值在本书第10章会专门讨论,而不可变类型在并发编程中能带来很多好处,没有数据竞争风险带来了更好的性能。一些语言(如Scala)就有明确的不可变类型声明,而Java中只能在定义类时将全部字段声明为final来间接实现。基本类型的范型支持是指在泛型中引用基本数据类型不需要自动装箱和拆箱,避免性能损耗。
-
Project Panama
目的是消弭Java虚拟机与本地代码之间的界线。现在Java代码可以通过JNI来调用本地代码,这点在与硬件交互频繁的场合尤其常用(譬如Android)。但是JNI的调用方式充其量只能说是达到能用的标准而已,使用起来仍相当烦琐,频繁执行的性能开销也非常高昂,Panama项目的目标就是提供更好的方式让Java代码与本地代码进行调用和传输数据。
文章作者 punk1u
上次更新 2021-06-23