OpenJDK原是SunMicroSystems公司(下面简称Sun公司)为Java平台构建的Java开发环境,于2009年4月15日由Sun公司正式发布。后来Oracle公司在2010年收购Sun公司,接管了这项工作。

随着OpenJDK的发布,越来越多的公司和组织都基于OpenJDK深度定制了一些独具特色的JDK分支,为用户提供更多选择。例如,国内厂商阿里巴巴的Dragonwell支持JWarmup,可以让代码在灰度环境预热编译后供生产环境直接使用;腾讯的Kona8将高版本的JFR和CDS移植到JDK8上;龙芯JDK支持包含JIT的MIPS架构,而非Zero的解释器版本;
国外厂商Amazon、Azul、Google、Microsoft、RedHat、Twitter等都有维护自用或者开源的JDK分支。
1)Amber:探索与孵化一些小的、面向生产力提升的Java语言特性。Amber项目的贡献包括模式匹配、Switch表达式、文本块、局部变量类型推导等语言特性。
2)Coin:决定哪些小的语言改变会添加进JDK7。常用的钻石形式的泛型类型推导语法以及try-with-resource语句都来自Coin项目。
3)Graal:Graal最初是基于JVMCI的编译器,后面进一步发展出GraalVM,旨在开发一个通用虚拟机平台,允许JavaScript、Python、Ruby、R、JVM等语言运行在同一个虚拟机上而不需要修改应用自身的代码。
4)Jigsaw:Jigsaw孵化了Java9的模块系统。
5)Kulla:实现一个交互式REPL工具,即JEP222的JShell工具。
6)Loom:探索与孵化JVM特性及API,并基于此构建易用、高吞吐量的轻量级并发与编程模型。目前Loom的研究方向包括协程、Continuation、尾递归消除。
7)Panama:沟通JVM和机器代码,研究方向有VectorAPI和新一代JNI。
8)Shenandoah:拥有极低暂停时间的垃圾回收器。相较并发标记的CMS和G1,Shenandoah增加了并发压缩功能。
9)Sumatra:让Java程序享受GPU、APU等异构芯片带来的好处。
10)Tsan:为Java提供ThreadSanitizer检查工具,可以检查Java和JNI代码中潜在的数据竞争。
11)Valhalla:探索与孵化JVM及Java的语言特性,主要贡献有备受瞩目的值类型(ValueType)、嵌套权限访问控制(Nest-basedAccessControl),以及对基本类型作为模板参数的泛型支持。
12)ZGC:低延时、高伸缩的垃圾回收器。它的目标是使暂停时间不超过10ms,且不会随着堆变大或者存活对象变多而变长,同时可以接收(包括但不限于)小至几百兆,大至数十T的堆。ZGC的关键字包括并发、Region、压缩、支持NUMA、使用着色指针、使用读屏障。

JEP(JavaEnhancementProposal)即Java改进提案。所谓提案是指社区在某方面的努力,比如在需要一次较大的代码变更,或者某项工作的目标、进展、结果值得广泛讨论时,就可以起草书面的、正式的JEP到OpenJDK社区。每个JEP都有一个编号,为了方便讨论,通常使用JEPID代替某个改进提案。
JEP之于Java就像PEP之于Python、RFC之于Rust,它代表了Java社区最新的工作动向和未来的研究方向。在较大的Java/JVM特性实现前通常都有JEP,它是Java社区成员和领导者多轮讨论的结果。JEP描述了该工作的动机、目标、详细细节、风险和影响等,通过阅读JEP(如果有的话),可以更好地了解Java/JVM某个特性。下面摘录了一些较新的JEP,注意,处于“草案”和“候选”状态的JEP不能保证最终会被加入JDK发行版。
1)JEP386(候选):将OpenJDK移植到AlpineLinux/x64。Alpine是一个极简的Linux发行版,作为Docker基础镜像,它的大小不到6MB,被广泛用于程序的云部署,但是Alpine使用musl作为C语言运行时库,与广泛使用的glibc有些出入,而JEP386可以很好地解决这个问题。
2)JEP378:Java文本块。文本块即多行的字符串字面值,其功能类似于其他编程语言的raw字符串功能,不需要为大多数特殊字符转义。将于JDK15发布。
3)JEP337(候选):让高性能计算和云端程序充分利用网络硬件并不容易,当前JDK的网络API使用操作系统内核的socket协议,在数据传输时涉及内核态和用户态的多次切换,会影响内存带宽和CPU周期。
为了改善这种情况,Java准备拟定实现rsocket协议,允许网络API访问远端内存(RDMA),提高吞吐量并降低网络延时。
4)JEP369:使用GitHub作为OpenJDK的Git仓库。
5)JEP384:提供Java记录支持。很多人都说Java不灵活,比如equals/hashCode等写起来太长了。在Spring或者一些RPC框架中,有时候仅仅想写一个单纯用作数据传输的类,也少不了要写一大堆重复方法和近乎刻板的getter/setter、toString、hashCode等方法,而且容易出错。
尽管IDE或者框架可以自动生成这些类,但是它们没有明确指出该类是POJO类,仅供数据传输使用。实际上,很多语言都可以声明只携带数据的类,如Scala的case类,Kotlin的data类以及C与CPU架构相关的代码├──os与CPU和操作系统相关的代码
└──share
├──adlcAOT支持,加载验证AOT库等
├──asmClient即时编译器(C1JIT)
├──ci字节码文件解析和处理
├──codeJIT编译器代理,虚拟机通过它选择特定的JIT编译器
├──gc一些JVM函数和常量的导出
├──interpreter诊断工具JavaFlightRecord
├──jvmci内部使用的数据结构
├──logging内存相关,包括内存划分,metaspace划分等
├──metaprogrammingJava类,对象在JVM中的表示
├──opto预编译文件
├──prims包罗万象的JVM运行时模块
├──services工具组件,如hashtable、JSON解析器、elf格式、快排算法等。
构建和调试
本文涉及的源码是jdk-12+31,操作系统为,CPU型号为IntelCorei7,JDK构建使用slowdebug类型(以下构建演示使用fastdebug类型)。如无特殊说明,文中均基于该配置分析和描述源码。
为了方便读者自行尝试,这里给出在三大主流操作系统上构建OpenJDK和断点调试HotSpotVM的方式。
1.在Windows上构建,用VisualStudio调试
下载并编译好freetype,然后安装cygwin及必要工具,如autoconf、make、zip、unzip,打开cygwin,进入源码目录输入命令进行编译,如代码清单1-1所示:
代码清单1-1Windows编译
生成的vs工程文件位于build目录下的ide/hotspot-visualstudio/中,使用VisualStudio双击载入即可,在菜单栏选择server-fastdebug即可开始调试。在调试时若遇到如图1-3所示的异常提示(safefetch32抛出异常),属于正常情况,继续调试即可。该异常会被外部SEH捕获。

图1-3VisualStudio调试
2.在macOS上构建,用Xcode调试
可以在macOS平台下载brew,然后使用brew安装hg、freetype、ccache,如代码清单1-2所示:
代码清单1-2macOS编译
在Linux开发机上可以使用VisualCode进行调试。VisualCode也是笔者推荐使用的智能编辑器,它同时支持Linux/Windows/macOS三大平台,只需简单的配置即可进行断点调试。
具体操作是在VisualCode菜单中选择File→Open,打开OpenJDK12源码目录,然后选择Debug→StartDebugging添加文件,如代码清单1-4所示:
代码清单1-4VisualCode的
{"version":"0.2.0","configurations":[{"cwd":"${workspaceFolder}","name":"HotSpotLinuxDebug","type":"cppdbg","request":"launch","program":"构建生成的JDK目录","args":["JVM启动参数"],"setupCommands":[{"description":"ignoresigsegv","ignoreFailures":false,"text":"handleSIGSEGVnostop"}]}]}
图1-5VisualCode调试
随着社区的不断发展,JDK的构建愈发成熟和简单,读者如果在构建过程中遇到问题,可以尝试根据报错自行解决,可以参见官方提供的构建文档(openjdk/doc/),也可以在互联网中寻求解决方案。构建一个可调试的虚拟机是探索虚拟机实现的第一步,也是必要的一步。
回归测试
当为虚拟机添加或者修改某些功能时,新增对应的测试是有必要的。常用的测试虚拟机和JDK的工具是jtreg。jtreg是JDK测试框架的一部分,它主要用于回归测试[1],当然也可以用于单元测试、功能测试等。
下面简单展示jtreg的使用方法。假设我们想为HotSpotVM新增一个虚拟机参数-XX:+DummyPrint,在开启时输出“HelloWorld”。为了实现该功能,可以在hotspot/share/runtime/文件中新增如代码清单1-5所示的代码:
代码清单1-5添加DummyPrint参数
develop(bool,DummyPrint,false,\"Printhelloworldonthescreen")\
然后在hotspot/share/runtime/的Threads::create_vm()函数的尾部增加一段代码,如代码清单1-6所示:
代码清单1-6DummyPrint功能实现
jintThreads::create_vm(JavaVMInitArgs*args,bool*canTryAgain){if(DummyPrint){tty-print_cr("HelloWorld");}returnJNI_OK;}修改完后,使用makehotspot增量式构建项目,然后附加虚拟机参数-XX:+DummyPrint进行测试,结果应该符合功能预期。但是要想确保新增的代码在较长的软件生命周期内正常运行,手动测试仍然显得太过麻烦。为了解决这个问题,可以使用自动回归测试。在openjdk/test/hotspot/jtreg/下新增测试文件,如代码清单1-7所示:
代码清单1-7
/**@testTestDummy*@summaryTestwhetherflag-XX:+DummyPrintworkscorrectly*@library/test/lib*@runmain/othervmTestDummy*@authorkelthuzadx*/;;publicclassTestDummy{staticclassWrap{publicstaticvoidmain(Stringargs){}}staticvoidrunWithFlag(booleanenableFlag)throwsThrowable{ProcessBuilderpb=(enableFlag?"-XX:+DummyPrint":"-XX:-DummyPrint",());OutputAnalyzerout=newOutputAnalyzer(());if(enableFlag){("HelloWorld");}else{("HelloWorld");}}publicstaticvoidmain(String[]args)throwsThrowable{runWithFlag(true);runWithFlag(false);}}自行构建jtreg或者下载预构建的jtreg,使用如代码清单1-8所示的命令进行测试:
代码清单1-8jtreg命令
$./jtreg-jdk:待测试的JDK路径openjdk/test/hotspot/jtreg/:passed:1
如果测试成功,则会看到passed字样,失败则会出现failed字样。可以在jtreg工作目录下的JTWork/日志文件中找到详细失败原因。
jtreg的核心是文件头注释中的各种符号,其中:@summary用于总结该测试的用途和测试内容;@library用于指定一个或多个路径名或者jar文件,如果是多个可使用空格隔开;@run用于指定以何种方式运行此测试。更多关于jtreg符号的详细用法可参见其相关文档。
GraalVM如果说HotSpotVM代表了传统的Java保守阵营,那么GraalVM无疑是Java改革阵营的代表。
大部分脚本语言或者有动态特性的语言(比如CPython、Lua、Erlang、Java、Ruby、R、JS、PHP、Perl、APL等)都需要用到一个语言虚拟机,但是这些语言的虚拟机实现差别很大,比如CPython/PHP的虚拟机性能相对较差,Java的HotSpotVM、C#的CLR和JS的v8却是业界顶尖级别。那么,能不能付出较小努力,用一个业界顶尖级别的虚拟机来运行这些语言,享受该虚拟机的一些工匠特性,如GC、锁优化、JIT编译器呢?
答案是肯定的。首先,对于Java、Scala、Groovy这些本来就是基于JVM的语言,通过编译器前端工具得到Java字节码后直接在JVM上运行即可。对于CPython、R、Ruby、PHP、Perl乃至自己写的一门新的语言,其开发流程一般分为如下4个阶段:
1)首先解析源代码到AST(AbstractSyntaxTree,抽象语法树),写一个AST解释器。
2)当有人使用这门语言时,语言设计者可以继续迭代,实现一个完整的语言虚拟机,包括GC、运行时等,代码的执行仍然使用AST解释器。
3)用的人多了,语言继续迭代,将AST转换为字节码,代码执行使用字节码解释器。4)用的人特别多,性能也很关键,如果这个语言社区有足够的资金和人力,那么可以写JIT编译器,提升GC性能等,不过大部分语言都到不了这一步。
一门语言至少要达到阶段3才算基本满足工业生产的要求,但是人们希望一门语言在阶段1时性能就足够好,而不用花那么多精力和财力达到阶段3甚至阶段4,这就是Truffle语言实现框架出现的原因。
Truffle将AST节点编译为机器代码使用的编译器是Graal,这是一个用Java编写的即时编译器。前面提到,Truffle是一个Java框架,那么一个用Java语言编写的即时编译器要如何编译Java代码呢?答案是通过JEP243的JVMCI。JVM是用C++语言编写的,在JVM中内置了两个用C++编写的即时编译器,C1和C2。一般频繁的代码先用C1编译,这些代码即热点,如果热点继续,则使用C2编译。JVMCI相当于把本该交给C2编译的代码交给Graal编译,然后使用编译后的代码。用Java写即时编译器看起来很神奇,其实很正常,因为即时编译说到底就是将一段byte[]代码在运行时转换为另一段byte[]代码,可以用任何语言实现,只是实现过程中的难易程度不同。
到目前为止,Java、Scala、Groovy已经可以在JVM上运行了,CPython、R、Ruby、JS通过Truffle框架实现一个AST解释器后也可以在JVM上运行。那么如何处理如C/C++、Go、Fortran这类静态语言呢?对于这个问题,GraalVM给出的解决方案是Sulong框架。用户用一些工具(如clang)将C/C++这类语言转换为LLVMIR,然后使用基于Truffle的AST解释器解释LLVMIR。这里基于Truffle的AST解释器就是Sulong,如图1-6所示。

图1-6Sulong(速龙,RapidDragon)
现在绝大部分语言都可以在JVM上运行了,将上面提到的所有技术放到一起,这个整体就叫作GraalVM。GraalVM就像皇帝的新衣,人人都在讨论,但是如果要回答它到底是什么却言之无物。实际上GraalVM这个语言虚拟机并不是真正存在的,GraalVM是指以Java虚拟机为基础,以Graal编译器为核心,以能运行多种语言为目标,包含一系列框架和技术的大杂烩,如图1-7所示。

图1-7GraalVM概览
但这并不是GraalVM的全部。图1-7中的所有语言最终都运行在JVM上,需要运行机器提前安装JDK环境。JVM由于自身原因,启动速度比较慢,内存负载较高。那么,能不能把程序直接打包成平台相关的可执行文件,后面直接执行这个可执行文件,而不依赖JVM呢?
交出这份答卷的是SubstrateVM。SubstrateVM借助Graal编译器,可以将Java程序AOT编译为可执行程序。它首先通过静态分析找到Java程序用到的所有类、方法和字段以及一个非常小的SVM运行时,然后对这些代码进行AOT编译,生成一个可执行文件。
SubstrateVM的想法很美好,但是在实践中会遇到诸多问题,因为Java有反射等动态特性,这些特性可能导致新类加载无法通过静态分析解决。目前SubstrateVM的GC是一个比较简单的分代GC,缺少很多调试工具和性能分析支持,编译速度较慢,不过这些都在慢慢完善,生产环境上也有阿里巴巴和Twitter等公司在不断尝试SubstrateVM的实际落地,并取得了显著的效果。
本章小结1.1节介绍了各具特色的JDK分支和OpenJDK的子项目。1.2节介绍了Java改进提案,它们代表类Java社区最新的工作动向。1.3节简单描述了历史长河中存在或者曾经存在的Java虚拟机。1.4节讨论了HotSpotVM的组件、源码结构、构建、调试以及修改代码后如何回归测试。最后1.5节展望未来,讨论了Java的前沿技术GraalVM。
本文给大家讲解的内容是Java生态系统,介绍JDK、JVM、JEP,带领大家走进虚拟机下篇文章给大家讲解的是详细类可用机制,类的加载、链接、初始化;
感谢大家的支持!