《HotSpot实战》—— 1.2 动手编译虚拟机

本节书摘来异步社区《HotSpot实战》一书中的第1章,第1.2节,作者:陈涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.2 动手编译虚拟机

源码面前,了无秘密。对于OpenJDK和HotSpot项目来说也是如此。因此,研究虚拟机实现机制的最佳途径就是阅读和调试源代码。我们希望能够动手编译一个完整的OpenJDK(含HotSpot)项目,或者仅编译HotSpot,这样就可以对虚拟机展开调试了。

虽然官方也支持在Windows操作系统下构建编译环境。但是经验表明,选择在Linux环境下搭建编译环境,可以避免不少弯路。理由有以下两点:

  • Windows上为了得到完整的编译环境,需要借助Cygwin等虚拟环境,而在Linux环境下环境下则可以省去大量的环境准备工作,成本较低;
  • 深入研究时,若遇到涉及操作系统内核上的困惑,开源的Linux内核较易获得。
    即便如此,编译一个完整的Open JDK对开发者的要求还是较高的。我们选择的Linux发行版、内核版本、编译器(GCC/G++等)以及项目依赖的第三方库的差异,都有可能导致编译过程出错。因此,需要读者具备基本的Linux使用技能,能够在出现问题时找到解决方案。

由于本文关注的只是HotSpot,所以编译完整的OpenJDK也并不是必须完成的任务,若无法编译完整的OpenJDK,我们可以仅编译HotSpot,这就能满足大部分的学习需求。相较于编译完整的OpenJDK,仅编译HotSpot项目就相对简单很多。简单说来,可以按照如图1-3所示的步骤进行操作。

《HotSpot实战》—— 1.2 动手编译虚拟机

(1)下载一套含有HotSpot项目的JDK源代码;

(2)搭建编译环境;

(3)配置编译目标;

(4)编译;

(5)运行测试程序,检测编译是否成功。

接下来,我们开始动手编译HotSpot吧。

1.2.1 源代码下载

可以在OpenJDK官网(http://download.java.net/openjdk/jdk7/)下载源代码。将下载的源代码包解压后,可以找到一个名为hotspot的目录,该路径下就是Hotspot项目完整的源代码。

1.2.2 HotSpot源代码结构

从JVM为语言的运行时提供支撑功能来看,虚拟机是Java语言的“系统程序”,但从本质上来说,它只是一个运行在操作系统上的普通应用程序而已。因此,对于我们来说,过分担心它有多么的庞大和神秘,是完全没有必要的。

HotSpot项目主体是由C++实现的,并伴有少量C代码和汇编代码。此外,作为HotSpot的重要组成部分之一, Serviceability Agent 1可维护性代理,简称SA)及其他Agent则由Java代码实现。

HotSpot工程目录结构如图1-4所示。

《HotSpot实战》—— 1.2 动手编译虚拟机

在根目录Hotspot下有Agent、Make、Src和Test目录。其中Make目录包含了编译HotSpot的Makefile文件,Agent目录中的源代码主要实现SA,而Test目录下包含了一些Java实现的测试用例。

在Src目录下就是HotSpot项目的主体源代码,由Cpu、Os、Os_cpu和Share这4个子目录组成。在Cpu目录下是一些依赖具体处理器架构的代码,主要按照Sparc、x86和Zero三种计算机体系结构划分子模块;Os目录下则是一些依赖操作系统的代码,主要按照Linux、Windows、Solaris和Posix2进行模块划分;而Os_cpu目录下则是一些同时依赖于操作系统和处理器类型的代码,如Linux+Sparc、Linux+x86、Linux+Zero、Solaris+Sparc、Solaris+x86和Windows+x86等模块。

在Share目录下是独立于操作系统和处理器类型的代码,这部分代码是HotSpot工程的核心业务,实现了HotSpot的主要功能。Share由两部分组成,一部分是实现虚拟机各项功能的Vm目录。另一部分是位于Tools目录下的几个独立的虚拟机工具程序,如Hsdis、IdealGraphVisualizer、Launcher、LogCompilation和ProjectCreator。

在Vm目录下,按照虚拟机的功能划分了一些模块。这些模块构成了虚拟机的内核,它们是HotSpot内核的顶层模块,每个顶层模块封装了在功能上相对独立的业务逻辑。目前,HotSpot中主要包括了下列顶层模块。

  • Adlc:平台描述文件。
  • Libadt:抽象数据结构。
  • Asm:汇编器。
  • Code:机器码生成。
  • C1:client编译器,即C1编译器。
  • Ci:动态编译器。
  • Compiler:调用动态编译器的接口。
  • Opto:Server编译器,即C2编译器。
  • Shark:基于LLVM实现的即时编译器。
  • Interpreter:解释器。
  • Classfile:Class文件解析和类的链接等。
  • Gc_interface:GC接口。
  • Gc_implementation:垃圾收集器的具体实现。
  • Memory:内存管理。
  • Oops:JVM内部对象表示。
  • Prims:HotSpot对外接口。
  • Runtime:运行时。
  • Services:JMX接口。
  • Utilizes:内部工具类和公共函数。

在下文中,我们将分别对这些模块展开探讨。第2章不仅介绍了Launcher作为JVM的启动器是如何启动虚拟机并进行系统初始化的,还介绍了Prims、Services和Runtime等公共模块为虚拟机提供的重要基础作用:Prims为外界与JVM搭建了通信的桥梁,Services和Runtime为其他模块提供了公共服务。第3章将会讨论Oops和Classfile模块,前者构成了HotSpot内部的面向对象表示系统,而后者则提供了类的解析功能;第4章会介绍Memory模块提供内存管理功能;第5章深入介绍了与垃圾收集器相关的GC模块;第7章详细讨论了与解释器和编译器的实现息息相关的Interpreter、C1、Code等模块。

1.2.3 搭建编译环境

在各种Linux发行版中,Ubuntu算是比较普及的一款产品。它具有功能丰富、更新速度快和容易上手的优良特性,鉴于此,我们选择Ubuntu 12作为开发环境。当然,也可以选择官方推荐的其他Linux发行版,如Fedora、Debian等,这完全没有任何限制。

我们在这里搭建的环境如下。

  • 源代码版本:OpenJDK7,分支代号b147。
  • 操作系统:基于Linux Kernel 3.5内核的Ubuntu 12.10(Vmware Workstation 9.03)发行版。
  • 编译环境:GCC 4.7 、G++ 4.6和GDB 7.5。

为方便搭建编译环境,读者可以在Hotspot目录下创建一个编译脚本,来节省许多手工配置工作。脚本内容如清单1-9所示(读者请将路径替换为本地路径)。

清单1-9
描述:HotSpot工程编译脚本

#!/bin/bash
export LANG=C```
导入JDK路径:

export ALT_BOOTDIR="/home/chentao/mywork/soft/jdk1.6.0_35"
export ALT_JDK_IMPORT_PATH="/home/chentao/mywork/soft/jdk1.6.0_35"`
导入ANT路径:

export ANT_HOME="/home/chentao/mywork/soft/apache-ant-1.8.4"```
导入PATH:

export
PATH="/usr/lib/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/ usr/games:/ home/chentao/mywork/soft/apache-ant-1.8.4:/usr/lib/i386-linux-gnu:/usr/lib/gcc/i686-linux-gnu/4.6"`
其他配置:

export  HOTSPOT_BUILD_JOBS=5 
#输出目录
export  ALT_OUTPUTDIR=../build/hotspot_debug```
选择目标版本为jvmg,启动编译HotSpot命令:

cd make
make jvmg jvmg1 2>&1 | tee ~/hotspot-in-action/hotspot/build/hotspot_debug.log`
编译完成后,在日志文件hotspot_debug.log中可以查看编译过程。阅读这份日志,有助于加深对HotSpot项目整体架构的理解。

1.2.4 编译目标

如清单1-10所示,在Makefile中定义了HotSpot项目的编译目标和级别。其中,主要包括以下4种基本级别。

  • product:产品级别。优化编译,但无断言。
  • fastdebug:快速调试级别。优化编译,但开启断言。
  • optimized:优化级别。优化编译,但无断言。
  • debug:调试级别。编译后的libjvm链接库中含有较丰富的调试信息。

清单1-10
来源:hotspot/make/Makefile
描述:编译目标

1   C1_VM_TARGETS=product1 fastdebug1 optimized1 jvmg1
2   C2_VM_TARGETS=product  fastdebug  optimized  jvmg
3   KERNEL_VM_TARGETS=productkernel fastdebugkernel optimizedkernel jvmgkernel
4   ZERO_VM_TARGETS=productzero fastdebugzero optimizedzero jvmgzero
5   SHARK_VM_TARGETS=productshark fastdebugshark optimizedshark jvmgshark

6   all:           all_product all_fastdebug
7   ifndef BUILD_CLIENT_ONLY
8   all_product:   product product1 productkernel docs export_product
9   all_fastdebug: fastdebug fastdebug1 fastdebugkernel docs export_fastdebug
10  all_debug:     jvmg jvmg1 jvmgkernel docs export_debug
11  else
12  all_product:    product1 docs export_product
13  all_fastdebug: fastdebug1 docs export_fastdebug
14  all_debug:      jvmg1 docs export_debug
15  endif
16  all_optimized: optimized optimized1 optimizedkernel docs export_optimized

17  allzero:              all_productzero all_fastdebugzero
18  all_productzero:    productzero docs export_product
19  all_fastdebugzero: fastdebugzero docs export_fastdebug
20  all_debugzero:      jvmgzero docs export_debug
21  all_optimizedzero: optimizedzero docs export_optimized

22  allshark:             all_productshark all_fastdebugshark
23  all_productshark:    productshark docs export_product
24  all_fastdebugshark: fastdebugshark docs export_fastdebug
25  all_debugshark:      jvmgshark docs export_debug
26  all_optimizedshark: optimizedshark docs export_optimized

在清单1-9中,我们在make命令后传递参数“jvmg jvmg1”,表示选择编译debug级别的目标。这样待编译成功后,生成的libjvm库(HotSpot VM运行时库)中会包含丰富的调试信息,通过这些信息,调试器可以建立虚拟机运行时与源代码间的关联,为单步调试HotSpot做好准备。

1.2.5 编译过程

执行清单1-9所示的编译脚本后,就可以启动HotSpot编译过程。如果一切顺利,待编译过程结束后,将在Hotspot目录下创建一个Build目录。Build目录是整个编译过程的工作空间,该目录下包含了最终的编译目标(参见清单1-10)。打开Build目录,可以见到一些新创建的目录,如清单1-11所示。

清单1-11

unix> cd build/
unix> ls
hotspot_debug  linux
unix> cd hotspot_debug/
unix> ls
linux_i486_compiler1  linux_i486_compiler2
unix> cd linux_i486_compiler1
unix> ls
**debug  fastdebug  generated  jvmg  optimized  product  profiled  shared_dirs.lst**```
编译结束后,执行jvmg目录下可执行文件test_gamma,便可以检验整个编译过程是否成功。执行test_gamma后,如果能够在控制台看到类似图1-5所示的输出信息,就表示编译成功了。
<div style="text-align: center"><img src="https://yqfile.alicdn.com/237f5258cc5175b5959301fb4a10ebe575f38a25.png" width="" height="">
</div>

实际上,test_gamma也是一个脚本,其内容如清单1-12所示。

清单1-12
来源:test_gamma
描述:测试脚本

1 #!/bin/sh
2 # Generated by /home/chentao/hotspot-in-action/hotspot/make/linux/makefiles/ buildtree.
make
3 . ./env.sh
4 if [ "" != "" ]; then { echo Cross compiling for ARCH , skipping gamma run.; exit 0; }; fi
5 if [ -z $JAVA_HOME ]; then { echo JAVA_HOME must be set to run this test.; exit 0; }; fi
6 if ! ${JAVA_HOME}/bin/java -d32 -fullversion 2>&1 > /dev/null
7 then
8 echo JAVA_HOME must point to 32bit JDK.; exit 0;
9 fi
10 rm -f Queens.class
11 ${JAVA_HOME}/bin/javac -d . /home/chentao/hotspot-in-action/hotspot/make/test/ Queens. java
12 [ -f gamma_g ] && { gamma=gamma_g; }
13 ./${gamma:-gamma} -Xbatch -showversion Queens < /dev/null`
在第3行中,执行env.sh准备执行环境。在第10行中,编译测试程序Queens.java。在第12和第13行中,最终是利用调试版启动器gamma,来启动测试程序Queens。Queens是一个求解N皇后问题的Java程序,图1-5便是运行Queens程序输出的结果。

1.2.6 编译常见问题

编译过程中可能会遇到一些问题,下面列出几个常见错误及其解决办法,供读者参考。

1.内核版本支持

在我们下载的HotSpot源代码中,默认支持的Linux内核最高版本为2.6,而我们所用的发行版很有可能采用了高于此版本的Linux内核。例如,笔者所用的Ubuntu12的内核是3.5(可通过uname -r命令查看自己内核版本)。如果不进行一些调整的话,编译HotSpot时可能会遇到如下报错:

"*** This OS is not supported:" 'uname –a'; exit 1;```
如果遇到这个问题,可以在这个文件中找到解决办法:hotspot/make/linux/Makefile。在Makefile文件中,定位到包含字符串“SUPPORTED_OS_VERSION”的代码,并在该行末尾增加“3.5%”,这样就可以使HotSpot支持我们实际使用的内核版本,调整后的代码如下:

SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 2.7% 3.5%

另一种调整方法是绕过验证操作系统版本的步骤。如清单1-13所示的定位到包含字符串“check_os_version”的代码,将其删除或者注释掉便可。

清单1-13
来源:hotspot/make/linux/Makefile
描述:验证OS版本

check_os_version:

ifeq ($(DISABLE_HOTSPOT_OS_VERSION_CHECK)$(EMPTY_IF_NOT_SUPPORTED),)

$(QUIETLY) >&2 echo "* This OS is not supported:" 'uname -a'; exit 1;

endif``

2.头文件的宏定义冲突的问题

cdefs.h中定义的宏“LEAF”与interfaceSupport.hpp冲突。可以在interfaceSupport.hpp中增加一个“#undef LEAF”语句来解决冲突,具体代码如清单1-14所示。

清单1-14
来源:hotspot/src/share/vm/runtime/interfaceSupport.hpp
描述:预定义宏__LEAF

// LEAF routines do not lock, GC or throw exceptions
**#ifdef __LEAF**
**#undef __LEAF**
#define __LEAF(result_type, header)                                  \
  TRACE_CALL(result_type, header)                                    \
  debug_only(NoHandleMark __hm;)                                     \
  /* begin of body */
#endif```
####3.GCC版本过高导致的问题
有时,编译器的版本也可能引起编译失败。例如,清单1-15描述了一个GCC版本过高引起的问题。

清单1-15

Linking vm...
/usr/bin/ld: cannot find -lstdc++
collect2: error: ld returned 1 exit status
ln: failed to create symbolic link 'libjvm_g.so': File exists
ln: failed to create symbolic link 'libjvm_g.so.1': File exists

GCC链接工具ld返回的错误信息显示:无法找到“-lstdc++”这个链接选项。这是由于GCC版本过高不支持“lstdc++”选项导致的错误。解决办法是把Makefile中的“lstdc++”选项去掉并重新尝试编译。
上一篇:带你读《互联网协议第六版 (IPv 6)》第二章IPv6 技术介绍2.2 IPv6 基本功能(二)


下一篇:带你读《互联网协议第六版 (IPv 6)》第二章IPv6 技术介绍2.2 IPv6 基本功能(一)