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

    xiaoxiao2023-12-17  157

    本节书摘来异步社区《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所示的步骤进行操作。

    (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下有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 makemake 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/sh2 # Generated by /home/chentao/hotspot-in-action/hotspot/make/linux/makefiles/ buildtree. make3 . ./env.sh4 if [ "" != "" ]; then { echo Cross compiling for ARCH , skipping gamma run.; exit 0; }; fi5 if [ -z $JAVA_HOME ]; then { echo JAVA_HOME must be set to run this test.; exit 0; }; fi6 if ! ${JAVA_HOME}/bin/java -d32 -fullversion 2>&1 > /dev/null7 then8 echo JAVA_HOME must point to 32bit JDK.; exit 0;9 fi10 rm -f Queens.class11 ${JAVA_HOME}/bin/javac -d . /home/chentao/hotspot-in-action/hotspot/make/test/ Queens. java12 [ -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 statusln: failed to create symbolic link 'libjvm_g.so': File existsln: failed to create symbolic link 'libjvm_g.so.1': File exists

    GCC链接工具ld返回的错误信息显示:无法找到“-lstdc++”这个链接选项。这是由于GCC版本过高不支持“lstdc++”选项导致的错误。解决办法是把Makefile中的“lstdc++”选项去掉并重新尝试编译。
    最新回复(0)