深入理解Java虚拟机(第3版)第一部分 走近java「详细笔记跳转专栏查看,持续更新中~谢谢支持」
本文内容:Java技术体系过去、现在的情况以及未来的发展趋势,在实践中介绍如何自己编译一个OpenJDK 12
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
周志明 著 ISBN:978-7-111-64124-7
书籍、网站资源
《Java虚拟机规范》、《Java语言规范》、《垃圾回收算法手册:自动内存管理的艺术》、《Virtual Machines:Versatile Platforms for Systems and Processes》、《Java性能优化权威指南》
第1章 走近Java
世界上并没有完美的程序,但我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过程。
1.1 概述
Java的技术体系主要由支撑Java程序运行的虚拟机、提供各开发领域接口支持的Java类库、Java编程语言及许许多多的第三方Java框架(如 Spring、MyBatis等)构成
Java获得广泛认可,优点:
- 结构严谨、面向对象的编程语言
- 它摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想
- 提供了一种相对 安全的内存管理和访问机制,避免了绝大部分内存泄漏和指针越界问题
- 实现了热点代码检测和运行时编译及优化,这使得Java应用能随着运行时间的增长而获得更高的性能
- 有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第三方类库来帮助用户实现各种各样的功能等
1.2 Java技术体系
广义上讲,Kotlin、Clojure、JRuby、Groovy等运行于Java虚拟机上的编程语言及其相关的程序都属于Java技术体系中的一员
传统意义上看JCP官方定义的Java技术体系包括:
- Java程序设计语言
- 各种硬件平台上的Java虚拟机实现
- Class文件格式
- Java类库API
- 来自商业机构和开源社区的第三方Java类库
按照领域、业务划分,Java技术体系包括:
- Java Card:支持Java小程序(Applets)运行在小内存设备(如智能卡)上的平台
- Java ME(Micro Edition):支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API 有所精简,并加入了移动终端的针对性支持,这条产品线在JDK 6以前被称为J2ME。Android并不属于Java ME
- Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API,这条产品线在JDK 6以前被称为J2SE
- Java EE(Enterprise Edition):支持使用多层架构的企业应用(如ERP、MIS、CRM应用)的Java平台,除了提供Java SE API外,还对其做了大量有针对性的扩充,并提供了相关的部署支持,这条产品线在JDK 6以前被称为J2EE,在JDK10以后被Oracle放弃,捐献给Eclipse基金会管理,此后被称为Jakarta EE
JRE与JDK:
- JRE(Java Runtime Environment):Java运行时环境,包含Java虚拟机(JVM)、Java基础类库以及支持文件等,是Java程序运行所需要的软件环境。JRE提供Java程序运行时的必要组件,使Java程序能够在不同的操作系统上运行。
- JDK(Java Development Kit):JDK是Java开发工具包,包含JRE。同时包含编译Java源码的编译器javac、Java程序调试和分析工具,是编写Java程序所需的开发工具包。JDK提供完整的Java开发环境,使开发者能够创建、编译、调试和运行Java应用程序。
1.3 Java发展史
1.4 Java虚拟机家族
虚拟机始祖:Sun Classic/Exact VM 世界上第一款商用Java虚拟机
武林盟主:HotSpot VM 热点代码探测能力
小家碧玉:Mobile/Embedded VM
天下第二:BEA JRockit/IBM J9 VM
软硬合璧:BEA Liquid VM/Azul VM
挑战者:Apache Harmony/Google Android Dalvik VM
没有成功,但并非失败:Microsoft JVM及其他
1.5 展望Java技术的未来
- 无语言倾向 Graal VM口号“Run Programs Faster Anywhere”「在任何地方都能更快地运行程序」。在HotSpot虚拟机基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用,发展潜力令人期待。如果Java语言或者HotSpot虚拟机真的有被取代的一天,那从现在看来Graal VM是希望最大的一个候选项
- 新一代即时编译器 JDK 10起,HotSpot中又加入了一个全新的即时编译器:Graal编译器,作为服务端编译器(简称为C2)替代者的身份登场
- 向Native迈进 提前编译 ,Substrate VM是在Graal VM 0.20版本里新出现的一个极小型的运行时环境,包括了独立的异常处理、同步调度、线程管 理、内存管理(垃圾收集)和JNI访问等组件,目标是代替HotSpot用来支持提前编译后的程序执行
- 灵活的胖子,经过一系列的重构与开放,HotSpot虚拟机逐渐从时间的侵蚀中挣脱出来,虽然代码复杂度还在增长,体积仍在变大,但其架构并未老朽,而是拥有了越来越多的开放性和扩展性,使得HotSpot成为一个能够联动外部功能,能够应对各种场景,能够学会十八般武艺的身手灵活敏捷的“胖子”
- 语言语法持续增强,新版本的Java中会出现越来越多其他语言里已有的优秀特性,相信博采众长的Java,还能继续保持现在的勃勃生机相当长时间!
1.6 实战:自己编译JDK
想要窥探Java虚拟机内部的实现原理,最直接的一条路径就是编译一套自己的JDK
1、 获源码
编译源码之前,我们要先明确OpenJDK和OracleJDK之间、OpenJDK的各个不同版本之间存在什么联系
OpenJDK是Sun公司在2006年年末把Java开源而形成的项目
在JDK 11以前,OracleJDK中还会存在一些OpenJDK没有的、闭源的功能,即OracleJDK 的“商业特性”。这些功能在JDK 11时才全部开源到了OpenJDK中。到了这个阶段,我们已经可以认为OpenJDK与OracleJDK代码实质上已达到完 全一致的程度。
OpenJDK和OracleJDK之间、OpenJDK的各个不同版本之间的关系如下图:
获取源码
- 过Mercurial代码版本管理工具从Repository中直接取得源 码(Repository地址:https://hg.openjdk.java.net/jdk/jdk12)「不科学上网会很慢」
- 直接访问准备下载的JDK版本的仓库页面(OpenJDK 12:https://hg.openjdk.java.net/jdk/jdk12/),点击左边菜单中的“Browse”, 点击左边的“zip”链接,到本地直接解压
个人网盘:链接: https://pan.baidu.com/s/1tFJjEskYsEwXRQ-sdddRKw 提取码: wyyz
2、系统需求
建议尽量在Linux或者MacOS上构建OpenJDK,采用64位操作系统,默认参数下编译出来的也是64位的OpenJDK。需要编译32位版本,同样推荐在64位的操作系统上进行,编译过程可以使用更大内存。
所有的文件,包括源码和依赖项目,都不要放在包含中文的目录里面
3、 构建环境
Ubuntu选择安装GCC或CLang进行编译。官方推荐使用GCC 7.8或者CLang 9.1
Ubuntu安装GCC的命令
sudo apt-get install build-essential
要编译版本号为N的JDK,还要准备至少为N-1的、已经编译好的JDK
Ubuntu中使用以下命令安装OpenJDK 11
sudo apt-get install openjdk-11-jdk
4、 编译
configure命令承担了依赖项检查、参数配置和构建输出目录结构等多项职责。
所有参数均通过以下形式使用: bash configure [options]
首先了解下OpenJDK提供的编译参数,使用命令查询:
bash configure--help
--with-debug-level=:设置编译级别,可选值:release、fastdebug、slowde-bug,越往后优化措施越少,调试信息就越多。一些虚拟机调试参数必须在特定模式下才可以使用。默认值为release
--enable-debug:等效于--with-debug-level=fastdebug
--with-native-debug-symbols=:确定调试符号信息的编译方式,可选值:none、 internal、external、zipped
--with-version-string=:设置编译JDK的版本号
--with-jvm-variants=[,...]:编译特定模式(Variants)的HotSpot虚拟机,可以多个模式并存,可选值:server、client、minimal、core、zero、custom
--with-jvm-features=[,...]:针对--with-jvm-variants=custom时的自定义虚拟机特性列表(Features),可以多个特性并存,可选值参见help命令输出
--with-target-bits=:指明要编译32位还是64位的Java虚拟机,在64位机器上可以交叉编译生成32位的虚拟机
--with-=:指明依赖包的具体路径,通常使用在安装了多个不同版本的Bootstrap JDK和依赖包的情况。lib可选值:boot-jd、freetype、cups、x、alsa、libffi、jtreg、libjpeg、 giflib、libpng、lcms、zlib
--with-extra-=:设定C、C++和Java代码编译时的额外编译器参数,其中 flagtype可选值:cflags、cxxflags、ldflags,分别代表C、C++和Java代码的参数
--with-conf-name=:指定编译配置名称,OpenJDK支持使用不同的配置进行编译,默认根据编译的操作系统、指令集架构、调试级别自动生成一个配置名称,譬如“linux-x86_64-serverrelease”,如果在这些信息都相同的情况下保存不同的编译参数配置,就需要使用这个参数来自定义配置名称
其他configure命令的部分参数使用“bash configure--help”查看
eg:编译FastDebug版、仅含Server模式的HotSpot虚拟机
bash configure --enable-debug --with-jvm-variants=server
编译过程中需要的工具链或者依赖项有缺失,命令执行后将会得到明确的提示,并且给出该依赖的安装命令,譬如以下例子所示:
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/配置名称”目录下产生如下目录结 构。多次编译,或者目录结构成功产生后又再次修改了配置,必须先使用“make clean”和“make dist-clean”命令清理目录,才能确保新的配置生效。编译产生的目录结构:
buildtools/:用于生成、存放编译过程中用到的工具
hotspot/:HotSpot虚拟机编译的中间文件
images/:使用make *-image产生的镜像存放在这里
jdk/:编译后产生的JDK就放在这里
support/:存放编译时产生的中间文件
test-results/:存放编译后的自动化测试结果
configure-support/、make-support/、 test-support/:这三个目录是存放执行configure、make和test的临时文件
依赖检查通过后,可以输入“make images”执行整个OpenJDK编译。「这里“images”是“productimages”编译目标(Target)的简写别名,这个目标的作用是编译出整个JDK镜像,除了“productimages”以外」
其他编译目标还有:
hotspot:只编译HotSpot虚拟机
hotspot-:只编译特定模式的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)
5、 在IDE工具中进行源码调试
采用的IDE是JetBrains的CLion 2019.1
新建项目->选择“New CMake Project from Sources”
在源码文件夹中填入OpenJDK源码根目录,此时,CLion已经自动选择好了需要导入的源码
点击OK按钮导入源码并自动创建好CMakeLists.txt文件
这份自动生成的CMakeLists.txt并不能直接使用,OpenJDK本身也没有为任何IDE提供支持。
但如果只是为了能够在CLion中跟踪、阅读源码,而不需要修改重新编译的话,那直接在Run/Debug Configurations中增加一个CMake Application ->
Executable选择刚才编译出来FastDebug或SlowDebug版的java命令 ->
运行参数加上-version或者某个Class文件的路径 ->
把Before launch里面的 Build去掉
就可以开始运行调试了,如图1-11所示:
目前需要在CLion中修改源码,并重新编译产生新的JDK,又或者不想阅读时看见一堆头文件缺失提示的话,需要把CMakeLists.txt修好,在GitHub上已经有现成的参考,可以直接下载,内容较多:
[https://github.com/ojdkbuild/ojdkbuild/blob/master/src/java-12-openjdk/CMakeLists.txt]
目前HotSpot在主流的操作系统上,都采用模板解释器来执行字节码,与即时编译器一样,最终执行的汇编代码都是运行期间产生的,无法直接设置断点,HotSpot增加了以下参数方便开发人员调试解释器:
-XX:+TraceBytecodes -XX:StopInterpreterAt=<n>
这组参数的作用是当遇到序号为的字节码指令时,便会中断程序执行,进入断点调试。调试解释器部分代码时,把这两个参数加到java命令的参数后面即可
完成以上配置之后,一个可修改、编译、调试的HotSpot工程就完全建立起来了,Hot-Spot虚拟机 启动器的执行入口是java.c的JavaMain()方法,读者可以设置断点单步跟踪,如图1-12所示:
本部分介绍了Java技术体系的过去、现在和未来的发展趋势,并在实践中介绍了如何自己编译一个 OpenJDK 12
作为全书的引言部分,本章建立了后文研究所必需的环境
后面将分为四部分去介绍Java在“自动内存管理”、“Class文件结构与执行引擎”、“编译器优化”、“多线程并发”方面的实现原理