想要一探 JDK 内部的实现机制,最便捷的捷径之一就是自己编译一套 JDK,通过阅读和跟踪调试 JDK 源码去了解 Java 技术体系的原理。本人选择了 OpenJDK 进行编译。

1. 获取 JDK 源码

获取 JDK 源码有两种方式:

(1). 通过 Mercurial 代码版本管理工具从 Repository 中直接取得源码

Repository 地址:http://hg.openjdk.java.net/jdk7u/jdk7u

获取过程如以下代码所示

1
2
3
4
hg clone http://hg.openjdk.java.net/jdk7u/jdk7u-dev
cd jdk7u-dev
chmod 755 get_source.sh
./get_source.sh

从版本管理中看变更轨迹效果较好,但不足之处是速度太慢,而且 Mercurial 不如 Git、SVN 等版本控制工具那样普及。

(2). 通过 OpenJDK™ Source Releases 页面取得打包好的源码

页面地址:https://download.java.net/openjdk/jdk7/

2. 系统需求

建议在 Linux、MacOS 或 Solaris 上构建 OpenJDK

本人采用的是 64 位操作系统,编译的也是 64 位的 OpenJDK

3. 构建编译环境

本人使用的是 MacOS ,需要安装最新版本的 XCode 和 Command Line Tools for Xcode,另外还要准备一个 6u14 以上版本的 JDK,官方称这个 JDK 为 “Bootstrap JDK” 。最后,需要下载一个 1.7.1 以上版本的 Apache Ant,用于执行 Java 编译代码中的 Ant 脚本。

4. 进行编译

最后我们还需要对系统的环境变量做一些简单设置以便编译能够顺利通过,这里给出使用的编译 Shell 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#语言选项,这个必须设置,否则编译好后会出现一个HashTable的NPE错
export LANG=C
#Bootstrap JDK的安装路径。必须设置
export ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home

#允许自动下载依赖
export ALLOW_DOWNLOADS=true

#并行编译的线程数,设置为和CPU内核数量一致即可
export HOTSPOT_BUILD_J0BS=8
export ALT_PARALLEL_COMPILE_JOBS=8

#比较本次build出来的映像与先前版本的差异。这对我们来说没有意义,
#必须设置为false,香则sanity检查会报缺少先前版本JDK的映像的错误提示。
#如桌已经设置dev或者DEV_ONLY=true,这个不显式设置也行
export SKIP_COMPARE_IMAGES=true

#使用预编译头文件,不加这个编译会更慢一些
export USE_PRECOMPILED_HEADER=true

#要编译的内容
export BUILD_LANGTOOLS=true
#export BUILD_JAXP=false
#export BUILD_JAXWS=fa1se
#export BUILD_CORBA=false
export BUILD_HOTSPOT=true
export BUILD_JDK=true

#要编译的版本
#export SKIP_DEBUG_BUILD=false
#export SKIP_FASTDEBUG_BUILD=true
#export DEBUG_NAME=debug

#把它设置为false可以避开java111`ws和浏览器Java插件之类的部分的build
BUILD_DEPLOY=false

#把它设置为false就不会build出安装包。因为安装包里有些奇怪的依赖,
#但即便不build出它也已经能得到完整的JDK映像,所以还是别build它好了
BUILD_INSTALL=false

#编译结果所存放的路径
export ALT_OUTPUTDIR=/Users/zjxjwxk/Documents/JVM/JDK-Build/build

#这两个环境变量必须去掉,不然会有很诡异的事情发生(我没有具体查过这些 "诡异的
#事情” ,Makefile脚本裣查到有这2个变量就会提示警告)
unset JAVA_HOME
unset CLASSPATH
make 2>&1 | tee $ALT_OUTPUTDIR/build.log

5. 检查

全部设置结束之后,可以输入 make sanity 来检查我们所做的设置是否全部正确。如果一切顺利,那么几秒钟之后会有类似代码清单1-2所示的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
~/Develop/JVM/jdkBuild/openjdk_7u4$ make sanity  
Build Machine Information:
build machine = IcyFenix-RMBP.local

Build Directory Structure:
CWD = /Users/IcyFenix/Develop/JVM/jdkBuild/openjdk_7u4
TOPDIR = .
LANGTOOLS_TOPDIR = ./langtools
JAXP_TOPDIR = ./jaxp
JAXWS_TOPDIR = ./jaxws
CORBA_TOPDIR = ./corba
HOTSPOT_TOPDIR = ./hotspot
JDK_TOPDIR = ./jdk

Build Directives:
BUILD_LANGTOOLS = true
BUILD_JAXP = true
BUILD_JAXWS = true
BUILD_CORBA = true
BUILD_HOTSPOT = true
BUILD_JDK = true
DEBUG_CLASSFILES =
DEBUG_BINARIES =

……因篇幅关系,中间省略了大量的输出内容……

OpenJDK-specific settings:
FREETYPE_HEADERS_PATH = /usr/X11R6/include
ALT_FREETYPE_HEADERS_PATH =
FREETYPE_LIB_PATH = /usr/X11R6/lib
ALT_FREETYPE_LIB_PATH =

Previous JDK Settings:
PREVIOUS_RELEASE_PATH = USING-PREVIOUS_RELEASE_IMAGE
ALT_PREVIOUS_RELEASE_PATH =
PREVIOUS_JDK_VERSION = 1.6.0
ALT_PREVIOUS_JDK_VERSION =
PREVIOUS_JDK_FILE =
ALT_PREVIOUS_JDK_FILE =
PREVIOUS_JRE_FILE =
ALT_PREVIOUS_JRE_FILE =
PREVIOUS_RELEASE_IMAGE = /Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home
ALT_PREVIOUS_RELEASE_IMAGE =

Sanity check passed.

Makefile 的 Sanity 检查过程输出了编译所需的所有环境变量,如果看到 “Sanity check passed” 说明检查过程通过了,可以输入 make 执行整个 OpenJDK 编译 (make 不加参数,默认编译 make all)。

编译完成后,进入 OpenJDK 源码下的 build/j2sdk-image 目录,这是整个 JDK 的完整编译结果,复制到 JAVA_HOME 目录,就可以作为一个完整的 JDK 使用,编译出来的虚拟机,在 -version 命令中带有用户的机器名。

1
2
3
4
> ./java -version  
openjdk version "1.7.0-internal-fastdebug"
OpenJDK Runtime Environment (build 1.7.0-internal-fastdebug-icyfenix_2012_12_24_15_57-b00)
OpenJDK 64-Bit Server VM (build 23.0-b21-fastdebug, mixed mode)

6. 单独编译 HotSpot 虚拟机

在大多数时候,如果我们不关心 JDK 中 HotSpot 虚拟机以外的内容,只想单独编译 HotSpot 虚拟机的话(例如调试虚拟机时,每次改动程序都执行整个 OpenJDK 的Makefile,速度肯定受不了),那么使用 hotspot/make 目录 下的 Makefile 进行替换即可,其他参数设置与前面是一致的,这时候虚拟机的输出结果存放在 build/hotspot/outputdir/bsd_amd64_compiler2 目录中,进入后可以见到以下几个目录。

1
2
3
4
5
6
7
0 drwxr-xr-x   15 IcyFenix  staff   510B 12 13 17:24 debug  
0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17:24 fastdebug
0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17:25 generated
0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17:24 jvmg
0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17:24 optimized
0 drwxr-xr-x 584 IcyFenix staff 19K 12 13 17:25 product
0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17:24 profiled

这些目录对应了不同的优化级别,优化级别越高,性能自然越好,但是输出代码与源码的差别就越大,难于调试,具体哪个目录有内容,取决于 make 命令后面的参数。

在编译结束之后、运行虚拟机之前,还要手工编辑目录下的 env.sh 文件,这个文件由编译脚本自动产生,用于设置虚拟机的环境变量,里面已经发布了 “JAVA_HOME、CLASSPATH、HOTSPOT_BUILD_USER” 3个环境变量,还需要增加一个“LD_LIBRARY_PATH”,内容如下:

1
2
LD_LIBRARY_PATH=.:${JAVA_HOME}/jre/lib/amd64/native_threads:${JAVA_HOME}/jre/lib/amd64:  
export LD_LIBRARY_PATH

然后执行以下命令启动虚拟机(这时的启动器名为gamma),输出版本号。

1
2
3
4
5
6
. ./env.sh  
./gamma -version
Using java runtime at: /Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home/jre
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b21)
OpenJDK 64-Bit Server VM (build 23.0-b21, mixed mode)

看到自己编译的虚拟机成功运行起来,很有成就感吧!

来源:《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

作者:周志明