公司动态
《深入理解java虚拟机》第1章:从零开始编译 OpenJDK 7 源码
1.6 实战自己编译 JDK想要一探 JDK 内部的实现机制最便捷的路径之一就是自己编译一套 JDK通过阅读和跟踪调试 JDK 源码去了解 Java 技术体系的原理虽然门槛会高一点但肯定会比阅读各种书籍、文章更加贴近本质。另外JDK 中的很多底层方法都是本地化Native的需要跟踪这些方法的运作或对 JDK 进行 Hack 的时候都需要自己编译一套 JDK。现在网络上有不少开源的 JDK 实现可以供我们选择如 Apache Harmony、OpenJDK 等。考虑到 Sun 系列的 JDK 是现在使用得最广泛的 JDK 版本笔者选择了 OpenJDK 进行这次编译实战。1.6.1 获取 JDK 源码首先要先明确 OpenJDK 和 Sun/Oracle JDK 之间以及 OpenJDK 6、OpenJDK 7、OpenJDK 7u 和 OpenJDK 8 等项目之间是什么关系这有助于确定接下来编译要使用的 JDK 版本和源码分支。从前面介绍的 Java 发展史中我们了解到 OpenJDK 是 Sun 在 2006 年末把 Java 开源而形成的项目这里的“开源”是通常意义上的源码开放形式即源码是可被复用的例如 IcedTea、UltraViolet 都是从 OpenJDK 源码衍生出的发行版。但如果仅从“开源”字面意义开放可阅读的源码上看其实 Sun 自 JDK 1.5 之后就开始以 Java Research LicenseJRL的形式公布过 Java 源码主要用于研究人员阅读JRL 许可证的开放源码至 JDK 1.6 Update 23 为止。把这些 JRL 许可证形式的 Sun/Oracle JDK 源码和对应版本的 OpenJDK 源码进行比较发现除了文件头的版权注释之外其余代码基本上都是相同的只有字体渲染部分存在一点差异Oracle JDK 采用了商业实现而 OpenJDK 使用的是开源的 FreeType。当然“相同”是建立在两者共有的组件基础上的Oracle JDK 中还会存在一些 OpenJDK 没有的、商用闭源的功能例如从 JRockit 移植改造而来的 Java Flight Recorder。预计以后 JRockit 的 MissionControl 移植到 HotSpot 之后也会以 Oracle JDK 专有、闭源的形式提供。Oracle 的项目发布经理 Joe Darcy 在 OSCON 2011 上对两者关系的介绍也证实了 OpenJDK 7 和 Oracle JDK 7 在程序上是非常接近的两者共用了大量相同的代码如图 1-6 所示注意图中提示了两者共同代码的占比要远高于图形上看到的比例所以我们编译的 OpenJDK基本上可以认为性能、功能和执行逻辑上都和官方的 Oracle JDK 是一致的。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 中也同步建立了 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。这是最直接的方式从版本管理中看变更轨迹比看 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 日发布大概 99 MB解压后约为 339 MB。1.6.2 系统需求如果可能笔者建议尽量在 Linux、MacOS 或 Solaris 上构建 OpenJDK这要比在 Windows 平台上容易得多本章实战中笔者将以 Ubuntu 10.10 和 MacOS X 10.8.2 为例进行构建。如果读者一定要在 Windows 平台上完成编译可参考本书附录 A该附录是本书第一版中介绍如何在 Windows 下编译 OpenJDK 6 的例子原有的部分内容现在已经过时了例如安装 Cygwin 部分但还是有一定参考意义因此笔者没有把它删除掉而是移到附录之中。无论在什么平台下进行编译都建议读者认真阅读一遍源码中的 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 至少需要 512 MB 的内存和 600 MB 的磁盘空间。512 MB 的内存也许能凑合使用不过 600 MB 的磁盘空间估计仅是指存放 OpenJDK 源码所需的空间要完成编译600 MB 肯定是无论如何都不够的光输出的编译结果就有近 3 GB因为有很多中间文件以及会编译出不同优化级别Product、Debug、FastDebug 等的虚拟机建议读者至少保证 5 GB 以上的空余磁盘。对系统的最后一点要求就是所有的文件包括源码和依赖项目都不要放在包含中文的目录里面这样做不是一定不可以只是没有必要给自己找麻烦。1.6.3 构建编译环境在 MacOS 和 Linux 上构建 OpenJDK 编译环境比较简单相对于 Windows 来说。对于 Mac OS需要安装最新版本的 XCode 和 Command Line Tools for XCode在 Apple Developer 网站Apple Developer上可以免费下载这两个 SDK 包提供了 OpenJDK 所需的编译器以及 Makefile 中用到的外部命令。另外还要准备一个 6u14 以上版本的 JDK因为 OpenJDK 的各个组成部分HotSpot、JDK API、JAXWS、JAXP、CORBA 等有的是使用 C 编写的更多的代码则是使用 Java 自身实现的因此编译这些 Java 代码需要用一个可用的 JDK官方称这个 JDK 为“Bootstrap JDK”。如果编译 OpenJDK 7Bootstrap JDK 必须使用 JDK 6 Update 14 或之后的版本笔者选用的是 JDK 7 Update 4。最后需要下载一个 1.7.1 以上版本的 Apache Ant用于执行 Java 编译代码中的 Ant 脚本。对于 Linux以 Ubuntu 10.10 为例所需要准备的依赖与 Mac OS 差不多Bootstrap JDK 和 Ant 都是一样的。在 MacOS 中 GCC 编译器来源于 XCode SDK而 Ubuntu 中 GCC 应该是默认安装好的需要确保版本为 4.3 以上如果没有找到 GCC安装 binutils 即可。在 Ubuntu 10.10 下编译 OpenJDK 7u4 所需的依赖可以使用以下命令一次安装完成sudo apt-get install build-essential gawk m4 openjdk-6-jdk libasound2-dev libcups2-dev libxrender-dev xorg-dev xutils-dev x11proto-print-dev binutils libmotif3 libmotif-dev ant1.6.4 进行编译现在需要下载的编译环境和依赖项目都准备齐全了最后我们还需要对系统的环境变量做一些简单设置以便编译能够顺利通过。OpenJDK 在编译时读取的环境变量有很多但大多都有默认值必须设置的只有两个LANG和ALT_BOOTDIR。必须设置的环境变量export LANGC否则在编译结束前的验证阶段会出现一个 HashTable 内的空指针异常。export ALT_BOOTDIR/Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home这是前面提到的 Bootstrap JDK 的安装路径在 Mac OS 上笔者设为以上路径其他操作系统读者对应调整即可。另外如果读者之前设置了JAVA_HOME和CLASSPATH两个环境变量在编译之前必须取消否则在 Makefile 脚本中检查到有这两个变量存在会有警告提示unset JAVA_HOME unset CLASSPATH其他环境变量笔者就不再一一介绍了代码清单 1-1 给出笔者自己常用的编译 Shell 脚本读者可以参考变量注释中的内容。代码清单 1-1 环境变量设置# 语言选项这个必须设置否则编译好后会出现一个 HashTable 的 NPE 错误 export LANGC # Bootstrap JDK 的安装路径。必须设置 export ALT_BOOTDIR/Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home # 允许自动下载依赖 export ALLOW_DOWNLOADStrue # 并行编译的线程数设置为和 CPU 内核数量一致即可 export HOTSPOT_BUILD_JOBS6 export ALT_PARALLEL_COMPILE_JOBS6 # 比较本次 build 出来的映像与先前版本的差异。这对我们来说没有意义 # 必须设置为 false否则 sanity 检查会报缺少先前版本 JDK 的映像的错误提示。 # 如果已经设置 dev 或者 DEV_ONLYtrue这个不显式设置也行 export SKIP_COMPARE_IMAGEStrue # 使用预编译头文件不加这个编译会更慢一些 export USE_PRECOMPILED_HEADERtrue # 要编译的内容 export BUILD_LANGTOOLStrue # export BUILD_JAXPfalse # export BUILD_JAXWSfalse # export BUILD_CORBAfalse export BUILD_HOTSPOTtrue export BUILD_JDKtrue # 要编译的版本 # export SKIP_DEBUG_BUILDfalse # export SKIP_FASTDEBUG_BUILDtrue # export DEBUG_NAMEdebug # 把它设置为 false 可以避开 javaws 和浏览器 Java 插件之类的部分的 build BUILD_DEPLOYfalse # 把它设置为 false 就不会 build 出安装包。因为安装包里有些奇怪的依赖 # 但即便不 build 出它也已经能得到完整的 JDK 映像所以还是别 build 它好了 BUILD_INSTALLfalse # 编译结果所存放的路径 export ALT_OUTPUTDIR/Users/IcyFenix/Develop/JVM/jdkBuild/openjdk_7u4/build # 这两个环境变量必须去掉不然会有很诡异的事情发生我没有具体查过这些“诡异的事情”Makefile 脚本检查到有这 2 个变量就会提示警告 unset JAVA_HOME unset CLASSPATH make 21 | tee $ALT_OUTPUTDIR/build.log全部设置结束之后可以输入make sanity来检查我们前面所做的设置是否全部正确。如果一切顺利那么几秒钟之后会有类似代码清单 1-2 所示的输出。代码清单 1-2 make sanity 检查~/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 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笔者使用 Core i7 3720QM/16GB RAM 的 MacBook 机器启动 6 条编译线程全量编译整个 OpenJDK 大概需 20 分钟编译结束后将输出类似下面的日志清单所示内容。如果读者之前已经全量编译过只修改了少量文件增量编译可以在数十秒内完成。# -- Build times ---------- Target all_product_build Start 2012-12-13 17:12:19 End 2012-12-13 17:31:07 00:01:19 corba 00:01:15 hotspot 00:00:14 jaxp 00:07:21 jaxws 00:08:11 jdk 00:00:28 langtools 00:18:48 TOTAL编译完成之后进入 OpenJDK 源码下的build/j2sdk-image目录或者build-debug、build-fastdebug这两个目录这是整个 JDK 的完整编译结果复制到JAVA_HOME目录就可以作为一个完整的 JDK 使用编译出来的虚拟机在-version命令中带有用户的机器名。 ./java -version openjdk version 1.7.0- internal- fastdebug OpenJDK Runtime Envi ronment (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)在大多数时候如果我们并不关心JDK中HotSpot虚拟机以外的内容只想单独编译HotSpot虚拟机的话(例如调试虚拟机时每次改动程序都执行整个OpenJDK的Makefile,速度肯定受不了)那么使用hotspot/make目录下的Makefile进行替换即可其他参数设置与前面是一-致的这时候虚拟机的输出结果存放在buildhotspot/outputdir/bsd_ amd64_compiler2目录9中进人后可以见到以下几个目录。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 optimi zed 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内容如下:LD_LIBRARY_PATH.:${JAVA_ HOME}/jre/lib/amd64/native_.threads:$ {JAVA_HOME}/jre/ 1ib/amd64: export LD_LIBRARY_PATH ; 然后执行以下命令启动虚拟机(这时的启动器名为gamma),输出版本号。 ../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) 在不同机器 上最后一个目录名称会有所差别bsd表示Mac oS系统(内核为FreeBSD) , amd64表示是 64位JDK (32位是x86)compiler2表示是Server VM (Client VM表示是compiler1) . 看到自己编译的虚拟机成功运行起来很有成就感吧!1.6.5 在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 jvmgALT_ BOOTDIR/Library/Java/JavaVirtualMachines/jdk1.7.0_ 04. jdk/ Contents/ HomeARCH_ DATA_ MODEL64 LANGCOpenJDK 7u4源码Makefile在终端运行时能正确获取到系统指令集架构为64位但在NetBeans中却没有取得正确的值误认为是32位因此这里必须使用ARCH_ DATA_MODEE参数明确指定为64位。另外两个参数ALT_ _BOOTDIR和LANG的作用前面已经介绍过。单击“完成”按钮HotSpot 项目就这样导人到NetBeans中了。不过这时候HotSpot还运行不起来,因为NetBeans根本不知道编译出来的结果放在哪里、哪个程序是虚拟机的入口等这些内容都需要明确告知NetBeans。在HotSpot工程上单击右键在弹出的快捷菜单中选择“属性”在弹出的对话框中找到“运行”选项设置运行命令为:/Users/IcyFenix/Develop/JVM/jdkBuild/openjdK_7u4/hotspot/build/bsd/bsd_amd64_compiber2/ j-vmg-/gamna Queens上面的Queens是MaKefile脚本自动产生的一段解八皇后问题的Java程序用壬测试虚拟机这里笔者直接拿来用了读者完全可以将它替换为自己的Java程序。读者在调试Java代码执行时如果要跟踪具体Java代码在虚拟机中是如何执行的也许会觉得无从下手因为目前在HotSpot主流的操作系统上都采用模板解释器来执行字节码它与HT编译器-样最终执行的汇编代码都是运行期间产生的无法直接设置断点所以HotSpot增加了以下参数来方便开发人员调试解释器。-XX: TraceBytecodes - XX:StopInterpreterAtn这组参数的作用是当遇到序号为n的字节码指令时便会中断程序执行进人断点调试。在调试解释器部分代码时把这两个参数加到gamma后面即可。最后还需要在“环境”窗日中设置环境变量也就是前面env.sh脚本所设置的那几个环境变量如图1-10 所示。完成以上配置之后一个可修改、编译、调试的HotSpot工程就完全建立起来了启动器的执行入口是java.c的main()方法读者可以设置断点单步跟踪如图1-11所示。由于HotSpot的源码比较长C/C 文件数量也很多为了便于读者阅读所以代码清单1-3给出了各个目录中代码的主要用途供读者参考。1.7 本章小结本章介绍了Java技术体系的过去、现在以及未来的一些发展趋势并通过实战介绍了如何自己来独立编译-一个OpenJDK 7.作为全书的引言部分本章建立了后文研究所必需的环境。在了解Java技术的来龙去脉后后面章节将分为4部分去介绍Java在内存管理、Class文件结构与执行引擎、编译器优化及多线程并发方面的实现原理。