实战:自己编译JDK

想要一探JDK内部的实现机制,最便捷的路径之一就是自己编译一套JDK ,通过阅读和跟踪调试JDK源码去了解Java技术体系的原理,虽然门槛会高一点,但肯定会比阅读各种书籍、文章更加贴近本质。另外,JDK中的很多底层方法都是本地化( Native ) 的 ,需要跟踪这些方法的运作或对JDK进行Hack的时候 ,都需要自己编译一套JDK。

现在网络上有不少开源的JDK实现可以供我们选择,如Apache Harmony、OpenJDK等。 考虑到Sun系列的JDK是现在使用得最广泛的JDK版 本 ,笔者选择了OpenJDK进行这次编译实战。

获取JDK源码

首先要先明确OpenJDK和Sun/OracleJDK之间 ,以及OpenJDK 6、OpenJDK 7 、OpenJDK 7u和OpenJDK 8等项目之间是什么关系,这有助于确定接下来编译要使用的JDK版本和源码分支。

从前面介绍的Java发展史中我们了解到OpenJDK是Sun在2006年末把Java开源而形成的项目,这里的“开源”是通常意义上的源码开放形式,即源码是可被复用的,例如IcedTea、Ultraviolet都是从OpenJDK源码衍生出的发行版。但如果仅从“开源”字面意义(开放可阅读的源码)上看,其实Sun自JDK 1.5之后就开始以Java Research License ( JRL)的形式公布过Java源码,主要用于研究人员阅读(JRL许可证的开放源码至JDK 1.6 Update 23为止)。把这些JRL许可证形式的Sun/OracleJDK源码和对应版本的OpenJDK源码进行比较,发现除了文件头的版权注释之外,其余代码基本上都是相同的,只有字体渲染部分存在一点差异, Oracle JDK采用了商业实现,而OpenJDK使用的是开源的FreeType。当然 ,“相同”是建立在两者共有的组件基础上的, Oracle JDK中还会存在一些Open JDK没有的、商用闭源的功能,例如从JRockitf多植改造而永的Java Flight Recorder。预计以后JRockit的MissionControl移植到HotSpot之后,也会以Oracle JDK专有、闭源的形式提供。

Oracle的项目发布经理Joe Darcy在OSCON 2011上对两者关系的介绍也证实了OpenJDK 7和Oracle JDK 7在程序上是非常接近的,两者共用了大量相同的代码(如图1-6所 示 ,注意图中提示了两者共同代码的占比要远高于图形上看到的比例),所以我们编译的OpenJDK, 基本上可以认为性能、功能和执行逻辑上都和官方的Oracle JDK是一致的。

再来看一下OpenJDK 6、OpenJDK 7、OpenJDK 7u和OpenJDK 8这几个项目之间的关系,
从图1-7 (依然是从Joe Darcy的OSCON 2011演示稿中截取的图片)来看, OpenJDK 7是始于 JDK 6时期,当时JDK 6和JDK 6 Update 1已经发布, JDK 7已经开始研发了,所以OpenJDK 7是直接基于正在研发的JDK 7源码建立的。但考虑到OpenJDK 7的状况在当时还不适合实际生产部署,因此在OpenJDK 7 Build 20的基础上建立了OpenJDK 6分支,剥离掉JDK 7新功能的代码,形成一个可以通过TCK 6测试的独立分支。

2012年7月 ,JDK 7正式发布,在OpenJDK 7中也同步建立了OpenJDK 7 Update对JDK 7进行更新升级,以及OpenJDK 8项目开始下一个JDK大版本的研发。按照开发习惯,新的功能或Bug修复通常是在最新分支上进行的,当功能或修复在最新分支上稳定之后会同步到其他老版本的维护分支上。

OpenJDK 6、 OpenJDK 7、 OpenJDK 7u和OpenJDK 8的源码都可以在它们相应的网页上找到 ,在本次编译实践中,笔者选用的项目是OpenJDK 7u ,版本为7u6。

获取OpenJDK源码有两种方式,其中一种是通过Mercurial代码版本管理工具从Repository中直接取得源码(Repository地址:http://hg.openjdk.java.net/jdk7u/jdk7u) ,获取过程如以下
代码所示。

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

这是最直接的方式,从版本管理中看变更轨迹比看Release Note效果更好。但不足之处是速度太慢,虽然代码总容量只有300 MB左 右 ,但是文件数量太多,在笔者的网络下全部复制到本地需要数小时。另外 ,考虑到Mercurial不如Git、SVN、ClearCase或CVS之类的版本控制工具那样普及,对于一般读者,建议采用第二种方式,即直接下载官方打包好的源码 包 ,读者可以从Source Bundle Releases页面(地址:http://jdk7.java.net/source.html )取得打包好的源码,到本地直接解压即可。一般来说,源码包大概一至两个月左右会更新一次,虽然不够及时,但比起从Mercurial复制代码的确方便和快捷许多。笔者下载的是OpenJDK 7 Update 6 Build b21版源码包,2012年8月28日发布,大概99MB,解压后约为339MB

系统需求

如果可能,笔者建议尽量在Linux、MacOS或Solaris上构建OpenJDK,这要比在Windows 平台上容易得多,本章实战中笔者将以Ubuntu 10.10和MacOS X 10.8.2为例进行构建。如果读者一定要在Windows平台上完成编译,可参考本书附录A ,该附录是本书第一版中介绍如何 在Windows下编译OpenJDK 6 的例子 ,原有的部分内容现在已经过时了(例如安装Plug部分 ),但还是有一定参考意义,因此笔者没有把它删除掉,而是移到附录之中。

无论在什么平台下进行编译,都建议读者认真阅读一遍源码中的README-builds.html文档 (无论在OpenJDK网站上还是在下载的源码包中都有这份文档),因为编译过程中需要注意的细节非常多。虽然不至于像文档上所描述的“Building the source code for the JDK requires a high level of technical expertise. Sun provides the source code primarily for technical experts who want to conduct research. ( 编译JDK需要很高的专业技术,Sun提供JDK源码是为了技术专家进行研究之用)”那么夸张,但是如果读者是第一次编译,那有可能会在一些小问题上耗费许多时间。

在本次编译中采用的是64位操作系统,编译的也是64位的OpenJDK,如果需要编译32位版本 ,那建议在32位操作系统上进行。在官方文档上写到编译OpenJDK至少需要512MB的内 存和600MB的磁盘空间。512MB的内存也许能凑合使用,不过600MB的磁盘空间估计仅是指存放OpenJDK源码所需的空间,要完成编译,600MB肯定是无论如何都不够的,光输出的编译结果就有近3GB ( 因为有很多中间文件,以及会编译出不同优化级别(Product、Debug、 FastDebug等 )的虚拟机),建议读者至少保证5GB以上的空余磁盘。

对系统的最后一点要求就是所有的文件,包括源码和依赖项目,都不要放在包含中文的目录里面,这样做不是一定不可以,只是没有必要给自己找麻烦。

1.6.3 构建编译环境

在MacOS和Linux上构建OpenJDK编译环境比较简单(相对于Windows来说 ),对于Mac OS,需要安装最新版本的XCode和Command Line Tools for XCode,在Apple Developer网站
( https://developer.apple.com/)上可以免费下载,这两个SDK包提供了OpenJDK所需的编译器以及Makefile中用到的外部命令。另外,还要准备一个6u14以上版本的JDK,因为OpenJDK的各个组成部分(Hotspot、JDK API、JAXWS、JAXP……)有的是使用C++编写的,更多的代码则是使用Java自身实现的,因此编译这些Java代码需要用到一个可用的JDK,官方称这个JDK为“Bootstrap JDK”。如果编译OpenJDK 7 , Bootstrap JDK必须使用JDK6 Update 14或之后的版本,笔者选用的是JDK7 Update 4。最后需要下载一个1.7.1以上版本的Apache Ant,用于执行Java编译代码中的Ant脚本。

对于Linux来说,所需要准备的依赖与Mac OS差不多, Bootstrap JDK和Ant都是一样的, 在Mac OS中GCC编译器来源于XCode SDK,而Ubuntu中GCC应该是默认安装好的,需要确保版本为4.3以上,如果没有找到GCC , 安装binutils即可,在Ubuntu10.10下编译OpenJDK7u4所需的依赖可以使用以下命令一次安装完成。

注意,只有在OpenJDK 7u4和之后的版本才能编译出Mac OS系统下的JDK包,之前的版本虽然在源码和编译脚本中也包含了Mac OS目录,但是尚未完善。

sudo apt-get install build-essential gawk m4 openjdk-6-jdk libasound2-dev libcups2-dev libxrender-dev xorg-dev xutils-dev xllproto-print-dev binutils libmotif3 libmotif-dev ant

1.6.4 进行编译

现在需要下载的编译环境和依赖项目都准备齐全了,最后我们还需要对系统的环境变量做一些简单设置以便编译能够顺利通过。OpenJDK在编译时读取的环境变量有很多,但大多都有默认值,必须设置的只有两个:LANG和ALT_BOOTDIR, 前者是设定语言选项,必须设置为:

export LANG=C

否则,在编译结束前的验证阶段会出现一个HashTable内的空指针异常。另外一个ALT_BOOTDIR参数是前面提到的Bootstrap JDK,在Mac OS上笔者设为以下路径,其他操作系统读者对应调整即可。


export ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdkl.7.0_04.jdk/Contents/Home

另外 ,如果读者之前设置了JAVA_HOME和CLASSPATH两个环境变量,在编译之前必须取消 ,否则在Makefile脚本中检查到看这两个变量存在,会有警告提示。

unset JAVA_HOME 
unset CLASSPATH

其他环境变量笔者就不再一一介绍了,代码清单1-1给出笔者自己常用的编译Shell脚本,读者可以参考变量注释中的内容。

代码清单1 - 1 环境变量设置

#语言选项,这个必须设置,否则编译好后会出现一个HashTable的NPE错
export LANG=C
#Bootstrap JDK的安装路径。必须设置
export ALT_BOOTDIR=/Librarv/Java/JavaVirtualMachines/jdkl.7 0_04.jdk/Contents/Home 

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

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

#比较本次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可以避开javaws和浏览器Java插件之类的部分的build
BUILD_DEPLOY=false

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

#编译结果所存放的路径
export ALT_OUTPUTDIR=/Users/IcyFenix/Develop/JVM/jdkBuiId/openjdk 7u4/build 

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

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

代码清单1-2 make sanity检查


Sanity check passed.

Makefile的Sanity检查过程输出了编译所需的所有环境变量,如果看到“Sanity check passed”,说明检查过程通过了,可以输入“make”执行整个OpenJDK编译 (make不加参数, 默认编译make all ) , 笔者使用Core i7 3720QM/16GB RAM的MacBook机器 ,启动6条编译线程 ,全量编译整个OpenJDK大概需20分钟,编译结束后,将输出类似下面的日志清单所示内容。如果读者之前已经全量编译过,只修改了少量文件,增量编译可以在数十秒内完成。

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

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

注意:在不同机器上,最号一个目录名称会有所差别,bsd表示Mac OS系统(内核为FreeBSD ) , amd64表示是64位JDK ( 32位是x86 ) ,compiler2表示是Server VM ( ClientVM表示是compiler1 )。

0 drwxr-xr-x15 IcyFenix staff 510B 12 13 17 :24 debug
0 drwxr-xr-x15 IcyFenix staff 510B 12 13 17 :24 fastdebug 
0 drwxr-xr-x15 IcyFenix staff 510B 12 13 17 :25 generated 
0 drwxr-xr-x15 IcyFenix staff 510B 12 13 17 :24 jvmg
0 drwxr-xr-x15 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、H0TSP0T_BUILD_USER”3个环境变量,还需要增加一个“LD_LIBRARY_PATH ,内容如下:

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

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

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

在IDE工具中进行源码调试

在阅读OpenJDK源码的过程中,经常需要运行、调试程序来帮助理解。我们现在已经可以编译出一个调试版本HotSpot虚拟机 ,禁用优化 ,并带有符号信息 ,这样就可以使用GDB 来进行调试了。据笔者了解,许多对虚拟机了解比较深的开发人员确实就是直接使用GDB加 VIM编辑器来开发、修改HotSpot的 ,不过相信大部分读者更倾向于在IDE环境而不是纯文本的GDB下阅读、跟踪HotSpot源码 ,因此这节就简单介绍一下“如何在IDE中进行HotSpot源码调试”。

首先,到NetBeans网站(http://netbeans.org/)上下载最新版的NetBeans,下载时选择支持 C/C++开发的那个版本。安装后 ,新建一个项目 ,选择“基于现有源代码的C/C++项目”,在 源码文件夹中填入OpenJDK目录下hotspot目录的路径,在下面的单选按钮中选择“定制”,如图1-8所 示 ,然后单击“下一步”按钮。

接着,在“指定构建代码的方法”中选择“使用现有的makefile” , 并填入Makefile文件的路 径 (在hotspot/make目录下),如图1-9所示。单击“下一步”按钮,将“构建命令”修改为以下内容:

${MAKE}-f Makefile clean jvmg ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdkl.7.0_04.jdk/Contents/Home ARCH_DATA_MODEL=64 LANG=C

OpenJDK 7u4源码Makefile在终端运行时能正确获取到系统指令集架构为64位 ,但在 NetBeans中却没有取得正确的值,误认为是32位 ,因此这里必须使用ARCH_DATA_MODEL 参数明确指定为64位。另外两个参数ALT_BOOTDIR和LANG的作用前面已经介绍过。单击“完成”按钮 ,HotSpot项目就这样导入到NetBeans中了。

不过 ,这时候HotSpot还运行不起来,因为NetBeans根本不知道编译出来的结果放在哪里、哪个程序是虚拟机的入口等,这些内容都需要明确告知NetBeans。在HotSpot工程上单击右键,在弹出的快捷菜单中选择“属性” ,在弹出的对话框中找到“运行”选项,设置运行命令为:

/Users/IcyFenix/Develop/JVM/jdkBuild/openjdk__7u4/hotspot/build/bsd/bsd_amd64_compiler2/jvrng/gamma Queens

上面的Queens是Makefile脚本自动产生的一段解八皇后问题的Java程序 ,用于测试虛拟机,这里笔者直接拿来用了,读者完全可以将它替换为自己的Java程序。

读者在调试Java代码执行时,如果要跟踪具体Java代码在虚拟机中是如何执行的,也许会觉得无从下手,因为目前在HotSpot主流的操作系统上,都采用模板解释器来执行字节码,它与JIT编译器一样,最终执行的汇编代码都是运行期间产生的,无法直接设置断点, 所以HotSpot增加了以下参数来方便开发人员调试解释器。

-XX:+TraceBytecodes-XX:StopInterpreterAt=<n>

这组参数的作用是当遇到序号为<n> 的字节码指令时,便会中断程序执行,进入断点 调试。在调试解释器部分代码时,把这两个参数加到gamma后面即可。
最后 ,还需要在“环境”窗口中设置环境变量,也就是前面erw.sh脚本所设置的那几个环境变量,如图1-10所示。

完成以上配置之后,一个可修改、编译、调试的HotSpot工程就完全建立起来了,启动器的执行入口是java.c的main( ) 方法,读者可以设置断点单步跟踪,如下所示。

由于Hotspot的源码比较长,C/C++文件数量也很多,为了便于读者阅读,所以代码清单1-3给出了各个目录中代码的主要用途,供读者参考。

HotSpot源码结构



Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐