深入jvm:jvm的体系结构以及如何编译一个jdk


简介

主要介绍体系结构,jvm的种类,在实践中介绍了如何自己编译一个OpenJDK 12。

体系结构

JCP官方所定义的Java技术体系包括了以下几个组成部分:

  • ·Java程序设计语言
  • ·各种硬件平台上的Java虚拟机实现
  • ·Class文件格式
  • ·Java类库API
  • ·来自商业机构和开源社区的第三方Java类库

Java程序设计语言、Java虚拟机、Java类库这三部分统称为JDK(Java DevelopmentKit)]。可以把Java类库API中的Java SE API子集[3]和Java虚拟机这两部分统称为JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境。

Java虚拟机实现

  • 第一代Java虚拟机:Sun Classic/Exact VM
  • 主流jvm::HotSpot VM
  • 其他:Mobile/Embedded VM, BEA JRockit/IBM J9 VM,BEA Liquid VM/Azul VM,Apache Harmony/Google Android Dalvik VM,Microsoft JVM。Substrate VM
  • 还有一些JavaME的虚拟机如 KVM
  • Graal VM(orcale):运行任何语言包括了Java、Scala、Groovy、Kotlin等基于Java虚拟机之上的语言,还包括了C、C++、Rust等基于LLVM的语言,同时支持其他像JavaScript、Ruby、Python和R语言等(是在HotSpot基础上诞生)

实战:自己编译jdk

jdk版本

1996年jdk1.0诞生,随着发张jdk1.0逐渐演化形成了openJDK,在2006年开源。随后许多不同的jdk诞生,如suiyoOracleJDK、Oracle、OpenJDK、AdoptOpenJDK、Azul Zulu、SAP SapMachine、Amazon Corretto、IcedTea、UltraViolet等都是从OpenJDK源码衍生出的发行版。jdk11之后为OpenJDK与OracleJDK代码实质上已达到完全一致的程度。

获取源码

方式一:OpenJDK 12的页面为https://hg.openjdk.java.net/jdk/jdk12/ ,然后点击左边菜单中的“Browse”,将显示如图源码根目录页面。

方式二:过Mercurial代码版本管理工具从Repository中直接取得源码(Repository地址:https://hg.openjdk.java.net/jdk/jdk12 ),命令 hg clone https://hg.openjdk.java.net/jdk/jdk12

编译

编译文档:阅读一遍源码中的doc/building.html文档
在MacOSLinux上构建OpenJDK编译环境相对简单,对于MacOS,需要MacOS X 10.13版本以上,并安装好最新版本的XCode和Command Line Tools for XCode(在Apple Developer网站[2]上可以免费下载),这两个SDK提供了OpenJDK所需的CLang编译器以及Makefile中用到的其他外部命令。对于Linux系统,要准备的依赖与MacOS类似,在MacOS中CLang编译器来源于XCode SDK,而Ubuntu里用户可以自行选择安装GCC或CLang来进行编译,但必须确保最低的版本为GCC 4.8或者CLang 3.2以上,官方推荐使用GCC 7.8或者CLang 9.1来完成编译。在Ubuntu系统上安装GCC的命令为:sudo apt-get install build-essential在编译过程中需要依赖FreeType、CUPS等若干第三方库,OpenJDK全部的依赖库已在表1-1中列出,执行相应的安装命令完成安装。
表1-1 OpenJDK编译依赖库
最后,假设要编译大版本号为N的JDK,我们还要另外准备一个大版本号至少为N-1的、已经编译好的JDK,这是因为OpenJDK由多个部分(HotSpot、JDK类库、JAXWS、JAXP……)构成,其中一部分(HotSpot)代码使用C、C++编写,而更多的代码则是使用Java语言来实现,因此编译这些Java代
码就需要用到另一个编译期可用的JDK,官方称这个JDK为“Bootstrap JDK”。编译OpenJDK 12时,Bootstrap JDK必须使用JDK 11及之后的版本。在Ubuntu中使用以下命令安装OpenJDK 11:sudo apt-get install openjdk-11-jdk

进行编译

需要下载的编译环境和依赖项目都齐备后,我们就可以按照默认配置来开始编译了,但通常我们编译OpenJDK的目的都不仅仅是为了得到在自己机器中诞生的编译成品,而是带着调试、定制化等需求,这样就必须了解OpenJDK提供的编译参数才行,这些参数可以使用“bash configure—help”命令查询到,笔者对它们中最有用的部分简要说明如下:

   ·--with-debug-level=<level>:
设置编译的级别,可选值为release、fastdebug、slowde-bug,越往后进行的优化措施就越少,带的调试信息就越多。还有一些虚拟机调试参数必须在特定模式下才可以使用。默认值为release。
·--enable-debug:等效于--with-debug-level=fastdebug。
·--with-native-debug-symbols=<method>:确定调试符号信息的编译方式,可选值为none、internal、external、zipped。
·--with-version-string=<string>:设置编译JDK的版本号,譬如java-version的输出就会显示该信息。这个参数还有--with-version-<part>=<value>的形式,其中part可以是pre、opt、build、major、minor、security、patch之一,用于设置版本号的某一个部分。
·--with-jvm-variants=<variant>[,<variant>...]:编译特定模式(Variants)的HotSpot虚拟机,可以多个模式并存,可选值为server、client、minimal、core、zero、custom。
·--with-jvm-features=<feature>[,<feature>...]:针对--with-jvm-variants=custom时的自定义虚拟机特性列表(Features),可以多个特性并存,由于可选值较多,请参见help命令输出。
·--with-target-bits=<bits>:指明要编译32位还是64位的Java虚拟机,在64位机器上也可以通过交叉编译生成32位的虚拟机。
·--with-<lib>=<path>:用于指明依赖包的具体路径,通常使用在安装了多个不同版本的BootstrapJDK和依赖包的情况。其中lib的可选值包括boot-jd、freetype、cups、x、alsa、libffi、jtreg、libjpeg、giflib、libpng、lcms、zlib。
·--with-extra-<flagtype>=<flags>:用于设定C、C++和Java代码编译时的额外编译器参数,其中flagtype可选值为cflags、cxxflags、ldflags,分别代表C、C++和Java代码的参数。
·--with-conf-name=<name>:指定编译配置名称,OpenJDK支持使用不同的配置进行编译,默认会根据编译的操作系统、指令集架构、调试级别自动生成一个配置名称,譬如“linux-x86_64-serverrelease”,如果在这些信息都相同的情况下保存不同的编译参数配置,就需要使用这个参数来自定义配置名称。

以上是configure命令的部分参数,其他未介绍到的可以使用“bash configure—help”来查看,所有参数均通过以下形式使用:
bash configure [options]
譬如,编译FastDebug版、仅含Server模式的HotSpot虚拟机,命令应为:
bash configure —enable-debug —with-jvm-variants=server
configure命令承担了依赖项检查、参数配置和构建输出目录结构等多项职责,如果编译过程中需要的工具链或者依赖项有缺失,命令执行后将会得到明确的提示,并且给出该依赖的安装命令,这比编译旧版OpenJDK时的“make sanity”检查要友好得多,譬如以下例子所示:

configure: error: Could not find fontconfig! You might be able to fix this by running 'sudo apt-get install libfconfigure exiting with result code 1
如果一切顺利的话,就会收到配置成功的提示,并且输出调试级别,Java虚拟机的模式、特性,使用的编译器版本等配置摘要信息,如下所示:

A new configuration has been successfully created in
/home/icyfenix/develop/java/jdk12/build/linux-x86_64-server-release
using default settings.
Configuration summary:
* Debug level: release
* HS debug level: product
* JVM variants: server
* JVM features: server: 'aot cds cmsgc compiler1 compiler2 epsilongc g1gc graal jfr jni-check jvmci jvmti mana* OpenJDK target: OS: linux, CPU architecture: x86, address length: 64
* Version string: 12-internal+0-adhoc.icyfenix.jdk12 (12-internal)
Tools summary:
* Boot JDK: openjdk version "11.0.3" 2019-04-16 OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu* Toolchain: gcc (GNU Compiler Collection)
* C Compiler: Version 7.4.0 (at /usr/bin/gcc)
* C++ Compiler: Version 7.4.0 (at /usr/bin/g++)
Build performance summary:
* Cores to use: 4
* Memory limit: 7976 MB

在configure命令以及后面的make命令的执行过程中,会在“build/配置名称”目录下产生如下目录结构。不常使用C/C++的读者要特别注意,如果多次编译,或者目录结构成功产生后又再次修改了配置,必须先使用“make clean”和“make dist-clean”命令清理目录,才能确保新的配置生效。编译产生的目录结构以及用途如下所示:

buildtools/:用于生成、存放编译过程中用到的工具
hotspot/:HotSpot虚拟机编译的中间文件
images/:使用make *-image产生的镜像存放在这里
jdk/:编译后产生的JDK就放在这里
support/:存放编译时产生的中间文件
test-results/:存放编译后的自动化测试结果
configure-support/:这三个目录是存放执行configure、make和test的临时文件
make-support/
test-support/

依赖检查通过后便可以输入“make images”执行整个OpenJDK编译了,这里“images”是“productimages”编译目标(Target)的简写别名,这个目标的作用是编译出整个JDK镜像,除了“productimages”以外,其他编译目标还有:

hotspot:只编译HotSpot虚拟机
hotspot-<variant>:只编译特定模式的HotSpot虚拟机
docs-image:产生JDK的文档镜像
test-image:产生JDK的测试镜像
all-images:相当于连续调用product、docs、test三个编译目标
bootcycle-images:编译两次JDK,其中第二次使用第一次的编译结果作为Bootstrap JDK
clean:清理make命令产生的临时文件
dist-clean:清理make和configure命令产生的临时文件

使用Oracle VM VirtualBox虚拟机,启动4条编译线程,8GB内存,全量编译整个OpenJDK 12大概需近15分钟时间,如果之前已经全量编译过,只是修改了少量文件的话,增量编译可以在数十秒内完成。编译完成之后,进入OpenJDK源码的“build/配置名称/jdk”目录下就可以看到OpenJDK的完整编译结果了,把它复制到JAVA_HOME目录,就可以作为一个完整的JDK来使用,如果没有人为设置过JDK开发版本的话,这个JDK的开发版本号里默认会带上编译的机器名,如下所示:
> ./java -version openjdk version "12-internal" 2019-03-19 OpenJDK Runtime Environment (build 12-internal+0-adhoc.icyfenix.jdk12) OpenJDK 64-Bit Server VM (build 12-internal+0-adhoc.icyfenix.jdk12, mixed mode)

在idea中进行调试

第一步

CLion安装后,新建一个项目,选择“New CMake Project from Sources”,在源码文件夹中填入OpenJDK源码根目录,此时,CLion已经自动选择好了需要导入的源码,如图1-10所示。点击OK按钮就会导入源码并自动创建好CMakeLists.txt文件。

第二步

自动生成的CMakeLists.txt并不能直接使用,OpenJDK本身也没有为任何IDE提供支持,但如果只是为了能够在CLion中跟踪、阅读源码,而不需要修改重新编译的话,那直接在Run/Debug Configurations中增加一个CMake Application,然后Executable选择我们刚才编译出来的FastDebug或者SlowDebug版的java命令,运行参数加上-version或者某个Class文件的路径,再把Before launch里面的Build去掉,就可以开始运行调试了

如果需要在CLion中修改源码,并重新编译产生新的JDK,又或者不想阅读时看见一堆头文件缺失提示的话,那还是需要把CMakeLists.txt修好,在GitHub上已经有现成的参考

HotSpot增加了以下参数来方便开发人员调试解释器:-XX:+TraceBytecodes -XX:StopInterpreterAt=
(是当遇到序号为的字节码指令时,便会中断程序执行,进入断点调试。调试解释器部分代码时,把这两个参数加到java命令的参数后面即可)

第三步

完成以上配置之后,一个可修改、编译、调试的HotSpot工程就完全建立起来了,Hot-Spot虚拟机
启动器的执行入口是java.c的JavaMain()方法,

资源

网站资源

  1. 高级语言虚拟机圈子:http://hllvm.group.iteye.com/。
  2. HotSpot Internals(OpenJDK的Wiki网站):https://wiki.openjdk.java.net/display/HotSpot/Main。
  3. The HotSpot Group(HotSpot虚拟机最新的讨论。):http://openjdk.java.net/groups/hotspot/。

深入理解Java虚拟机作者联系

读者邮箱:under-standingjvm@gmail.com


文章作者: Needle
转载声明:本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Needle !
  目录
  评论