JVM :从手动编译 JDK 开始

3,026 阅读8分钟

JDK,全名 Java Development Kit ,包含了从事 Java 程序开发所需的基本工具。我们在各大平台中所使用的 JDK ,都是由 Oracle 提供的通用版本。由于仅需要简单地环境变量,就可以在上层进行 Java 编程,因此笔者层忽略了 JDK 内部的诸多细节。

如果我们可以弄清楚 JDK 具体包含了什么,那么在日后我们甚至可以通过源码对 JDK 本身进行一些微小的调整。笔者最近入手了周志明所著的《深入理解 Java 虚拟机》,并计划沿着该书的大纲内容对 JVM 虚拟机做深入了解。

在一窥 JVM 运行机理之前,笔者选择先从手动编译 JDK 开始。建议在开始编译之前先阅读一下 Open Jdk 官方文档:OpenJDK Build README

这篇博主提供了非常清晰的思路:编译 OpenJDK 8 。另外,选择 Ubuntu 环境进行编译的同学也可以参考这篇文章:来,手把手教你编译OpenJDK源代码

前置准备

笔者在 CentOS 7 虚拟机环境编译 JDK 。注意,最好为此虚拟机分配尽可能多的内存(笔者推测在编译过程当中会产生大量的中间内容),否则会在编译期间产生内存无法分配的问题,或者严重降低编译效率

注意,我们需要做两手准备:

  1. JDK 8 源码百度云下载 提取码:74fe

  2. JDK 7 百度云下载 提取码:dxky

为什么会这样呢?我们要编译的 OpenJDK 里除了 HotSpot 虚拟机是由 C/C++ 语言编写之外,大部分内容仍然是 Java 代码,因此我们依赖一个编译期间可以正常运行的 Java 环境,即“编译 JDK 的 JDK ”,又称之为 "BootStrap JDK" 。

如果我们要编译版本为 N 的 JDK ,则需要一个至少为 N-1 版本的 JDK 作为 BootStrap JDK 。显然,这里我们的目标 JDK 版本为 8,而 BootStrap JDK 为 7。

在下载 JDK 之后,不要着急配置 JAVA_HOME 环境变量。相反,如果系统已经提前为你准备好了 JAVA_HOME则首先需要移除相关配置。(CentOS 系统可以通过 env 命令快速检查环境变量)

在下载完上述两个压缩包之后,使用tar -zxvf 解压 JDK 7 的 .gz 包,使用 unzip 解压 JDK 8 源码的 .zip 包(此命令默认解压到当前目录下)

OpenJDK 目录结构

下面是 OpenJDK 的目录结构,从中我们可以一窥到 JDK 所包含的组件,包括 Java 的核心内容 —— 虚拟机。

openjdk 
—— corba:多语言、分布式通讯接口 
—— hotspot:Java 虚拟机 
—— jaxp:XML 处理 
—— jaxws:一组 XML web services 的 Java API 
—— jdk:java 开发工具包 
—— langtools:Java 语言工具 
—— nashorn:JVM 上的 JavaScript 运行时

OpenJDK 与 Oracle JDK

我们在这里将使用 OpenJDK 用于编译练习,而之前,我们都是从 Oracle 官网中直接下载并使用 Oracle JDK。 Java 的发展历程背后是诸多科技公司之间的“宫斗史”,笔者在这里不会再提及。

这里仅简单叙述 OpenJDK 和 Oracle JDK 之间的联系: OpenJDK 是 Sun 公司在 2006 年将 Java 开源的项目。各大科技公司都具有基于 OpenJDK 进行二次开发后的独立 JDK,包括 IBM 公司的 J9,也包括 Oracle 公司的 Oracle JDK。

OpenJDK 和 Oracle JDK 从使用上几乎感觉不出任何区别,毕竟都是一个模子里刻出来的。然而,由于产权问题,OpenJDK 不得不将一些被迫闭源的内容替换成了自己的开源实现。除此之外, OpenJDK 只包含了最精简的软件包,比如 Rhino , Java DB ...... 但你仍然可以选择在需要的时候将它们补充进去。

所以不用太担心,无论是何种 XX JDK ,它都应该能保证运行基本的 Java 代码,否则就不能称之为 JDK。

HotSpot 虚拟机目录结构

HotSpot 是目前最通用的 Java 虚拟机,其中,HotSpot VM 的实现在 hotspot/src 目录。其结构如下所示:

├─agent                            Serviceability Agent的客户端实现
├─make                             用来build出HotSpot的各种配置文件
├─src                              HotSpot VM的源代码
│  ├─cpu                            CPU相关代码(汇编器、模板解释器、ad文件、部分runtime函数在这里实现)
│  ├─os                             操作系相关代码
│  ├─os_cpu                         操作系统+CPU的组合相关的代码
│  └─share                          平台无关的共通代码
│      ├─tools                        工具
│      │  ├─hsdis                      反汇编插件
│      │  ├─IdealGraphVisualizer       将server编译器的中间代码可视化的工具
│      │  ├─launcher                   启动程序“java”
│      │  ├─LogCompilation             将-XX:+LogCompilation输出的日志(hotspot.log)整理成更容易阅读的格式的工具
│      │  └─ProjectCreator             生成Visual Studio的project文件的工具
│      └─vm                           HotSpot VM的核心代码
│          ├─adlc                       平台描述文件(上面的cpu或os_cpu里的*.ad文件)的编译器
│          ├─asm                        汇编器接口
│          ├─c1                         client编译器(又称“C1”)
│          ├─ci                         动态编译器的公共服务/从动态编译器到VM的接口
│          ├─classfile                  类文件的处理(包括类加载和系统符号表等)
│          ├─code                       动态生成的代码的管理
│          ├─compiler                   从VM调用动态编译器的接口
│          ├─gc_implementation          GC的实现
│          │  ├─concurrentMarkSweep      Concurrent Mark Sweep GC的实现
│          │  ├─g1                       Garbage-First GC的实现(不使用老的分代式GC框架)
│          │  ├─parallelScavenge         ParallelScavenge GC的实现(server VM默认,不使用老的分代式GC框架)
│          │  ├─parNew                   ParNew GC的实现
│          │  └─shared                   GC的共通实现
│          ├─gc_interface               GC的接口
│          ├─interpreter                解释器,包括“模板解释器”(官方版在用)和“C++解释器”(官方版不在用)
│          ├─libadt                     一些抽象数据结构
│          ├─memory                     内存管理相关(老的分代式GC框架也在这里)
│          ├─oops                       HotSpot VM的对象系统的实现
│          ├─opto                       server编译器(又称“C2”或“Opto”)
│          ├─prims                      HotSpot VM的对外接口,包括部分标准库的native部分和JVMTI实现
│          ├─runtime                    运行时支持库(包括线程管理、编译器调度、锁、反射等)
│          ├─services                   主要是用来支持JMX之类的管理功能的接口
│          ├─shark                      基于LLVM的JIT编译器(官方版里没有使用)
│          └─utilities                  一些基本的工具类
└─test                             单元测试

准备编译工具并编译

我们可以通过 yum 工具快速安装编译所需的工具:

$ sudo yum groupinstall "Development Tools"  \
yum install libXtst-devel libXt-devel libXrender-devel  \
yum install cups-devel  \
yum install freetype-devel \  
yum install alsa-lib-devel  

在一切准备就绪之后,我们需要进入到 openjdk 的根目录下,并准备一些配置信息:

$ sudo cd yourOpenJdk
$ sudo bash ./configure --with-target-bits=64 --with-boot-jdk=yourBootJdk --with-debug-level=slowdebug --enable-debug-symbols ZIP_DEBUGINFO_FILES=0

下面是对参数的简单介绍:

  1. with-target-bits:指定生成的是 64 或者是 32 位的 jdk。
  2. with-boot-jdk指向你自己的 BootStrap JDK 的实际路径
  3. with-debug-level:编译时的 debug 级别,分为 release , fastdebug , slowdebug 三种。默认是 release。
  4. enable-debug-symbols:生成调试的符号信息。

configure 命令承担了对依赖性检查,参数配置等职责。如果某些工具或者依赖项缺失,该命令会给出明确的提示。如果 configure 命令没有发生问题,我们就可以使用 make 命令进行编译了。

$ sudo make all ZIP_DEBUGINFO_FILES=0

笔者为虚拟机分配了一个 CPU 和 3 GB 内存空间,编译过程等待了较长的时间(笔者初期仅仅分配了 1GB 内存空间,结果导致编译过程中报内存分配失败错误,因此不得不在调整虚拟机配置后重新进行编译)。在编译过程中可能会报多处 warnings,但是只要不是导致中断编译的 warnings ,一般不会影响编译结果。

此外,编译过程中控制台可能还会打印以下错误,此为编译过程本身的一个 bug,我们可以暂时将其忽略。

Generating Nimbus source files
Verifying /u/alanb/ws/tl/build/linux-x86_64-normal-server-release/jdk/gensrc_x11wrappers/sizes.64.verification.tmp to /u/alanb/ws/tl/build/linux-x86_64-normal-server-release/jdk/gensrc_x11wrappers/sizes.64
[Error] encoded value was less than 0: encode(-8.326673E-17, 5.0, 11.0, 16.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was greater than 3: encode(15.029411, 1.0, 14.0, 15.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was greater than 3: encode(15.029411, 1.0, 14.0, 15.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was greater than 3: encode(15.029411, 1.0, 14.0, 15.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was greater than 3: encode(15.029411, 1.0, 14.0, 15.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was greater than 3: encode(15.029411, 1.0, 14.0, 15.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] encoded value was greater than 3: encode(15.029411, 1.0, 14.0, 15.0)
[Error] encoded value was less than 0: encode(-0.05882353, 1.0, 24.0, 25.0)
[Error] Encountered Infinity: encode(-0.00877193, 0.0, 7.0, 7.0)

对该问题的解答请参阅此链接。最终,编译完成后控制台会输出编译时间:

----- Build times -------
Start 2020-08-12 04:26:55
End   2020-08-12 04:40:23
00:00:00 corba
00:00:55 demos
00:05:08 docs
00:00:01 hotspot
00:01:47 images
00:00:01 jaxp
00:00:01 jaxws
00:04:58 jdk
00:00:02 langtools
00:00:35 nashorn
00:13:28 TOTAL

后续工作

在编译完成之后,BootStrap JDK 就完成了它的使命,在编译完成之后我们便可以卸载掉 BootStrap JDK 了。

$ sudo yum list installed | grep java
$ sudo yum -y remove java-1.7.0-openjdk*

编译好的 JDK 会在 ../openjdk/build/linux-x86_64-normal-server-slowdebug/jdk 文件夹下。由于编译时 config 的参数可能并不一致,因此编译后输出的文件夹名称可能也有所区别。

我们此刻再用这个编译好的 JDK 去配置 Java 环境变量:

$ sudo vim /etc/profile

增加以下配置,注意 JAVA_HOME 应当指向你实际的文件夹路径,这里仅以笔者的为准。配置完毕后不要忘记使用source /etc/profile 刷新一下配置:

JAVA_HOME=/root/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/
JRE_HOME=$JAVA_HOME/jre
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
export JAVA_HOME JRE_HOME PATH CLASSPATH

使用 java -version 打印结果,笔者这里显示:

openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

而直接安装 JDK 的机器会显示:

java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

我们可以准备一个测试用的 JvmTest.java 文件,观察此 openJDK 是否能够正常工作:

public class JvmTest {  
 public static void main(String[] args) {  
  System.out.println(System.getProperty("user.home"));  
  System.out.println(System.getProperty("java.version"));  
  System.out.println(System.getProperty("os.name"));  
  System.out.println(System.getProperty("java.vendor.url"));  
 }  
}

如果 java , javac 均没有问题,则控制台可以输出以下内容:

/root
1.8.0_181
Linux
http://java.oracle.com/