1.6 抛砖引玉:编译和调试虚拟机
如果要对虚拟机进行深入的研究,那么编译和调试Java虚拟机是必不可少的。为何要编译自己的虚拟机呢?主要原因有两点。
第一,通过自己编译可以得到一个debug或者fastdebug版本的调试用虚拟机,调试用虚拟机可以支持更多的虚拟机参数,这些开发专用的虚拟机参数可以帮助开发人员获得更多的虚拟机内部信息,而这些参数在正常发行版本中是无法使用的。因此考虑到本书的实用性,本书并不会过多介绍那些只在调试版本中使用的参数,但如果读者有兴趣,可以编译自己的虚拟机,进行尝试。
第二,使用自己编译的调试版虚拟机可以进行虚拟机代码的单步调试,有利于实现对虚拟机代码的理解。由于虚拟机代码比较复杂,仅通过代码阅读很难深入理解其实现机制,有时不得不依靠单步调试,而编译后调试版本可以帮助你实现这一功能。
为编译虚拟机,首先必须获得虚拟机源码,读者可以使用下面的命令获取JDK10的源码。推荐读者使用较新的版本,因为老版本的编译脚本可能在某些平台上存在问题。
笔者的编译环境为Ubuntu系统,读者可以选择自己喜欢的Linux发行版进行编译。编译虚拟机之前,还必须做一些准备工作。
1.编译前的准备工作
(1)安装好依赖库。在Ubuntu平台上可以通过apt-get命令安装,在CentOS平台上可以通过yum命令安装。比如,笔者在编译前至少安装了以下库:
如果依赖库安装不全,在编译过程中就会提示错误,从错误提示中,读者应该可以得知缺少了哪些库或者头文件,进行安装即可。当然,gcc和g++作为基本的编译工具也是必须要安装的。
(2)准备一个Boot JDK。Boot JDK用于OpenJDK编译的执行。笔者在这里使用的是JDK8,也推荐读者使用JDK8作为JDK10的Boot JDK。
2.准备编译
准备就绪之后,就可以开始编译了。
(1)进入解压后的openjdk目录:
(2)执行configure脚本配置编译选项,笔者的配置如下:
在该脚本中,-with-debug-level=slowdebug代表要编译debug版本JDK;-enable-dtrace代表开启dtrace;-with-target-bits=64代表编译64位JVM;-with-memory-size=3000代表编译JDK的计算机至少需要3GB,根据计算机不同配置可以设置不同的值;-disable-warnings-as-errors代表忽略警告的信息;-enable-native-debug-symbols=internal代表生成symbol文件,这个便于后续的动态调试;-with-boot-jdk=/opt/jdk1.8.0_181/代表Boot JDK是JDK1.8。更多的编译选项可以参考JDK源码根目录的README文件。
配置成功会显示下面的信息:
(3)通过make images命令执行整个编译,将会生成debug版本的虚拟机。编译的过程可能会花费比较长的时间,一般来说,编译一个版本可能需要15~45分钟,视计算机性能而定。当编译成功后,会有以下输出:
进入build目录,可以看到编译的结果,下面显示了debug版本的编译结果:
有了debug版本的虚拟机,就可以使用gdb对虚拟机进行调试了。接下来将简单地演示Java虚拟机的调试方法。
3.Java虚拟机的调试
笔者选用linux-x86_64-normal-server-slowdebug下的虚拟机对虚拟机的源码进行调试。
(1)进入linux-x86_64-normal-server-slowdebug/jdk/bin目录,用gdb启动Java可执行程序。
(2)进入gdb命令行环境:
在main函数中打断点:
执行run,继续运行:
可以看到,当前程序停在了java.base/share/native/launcher/main.c文件的第97行。使用命令next(缩写为n)进行单步调试。
至此,读者就可以使用这套环境作为辅助,更好地深入Java虚拟机内部了。