要分析 JVM 的源码, 结合资料直接阅读是一种方式, 但是遇到一些想不通的场景, 必须要结合调试, 查看执行路径以及参数具体的值, 才能搞得明白. 所以我们先来把 JVM 的源码进行编译, 并能够使用 GDB 进行调试.
编译环境
本文使用的 JDK 版本: OpenJDK7, 分支 b147
下载页面: https://download.java.net/openjdk/jdk7
下载地址: http://download.java.net/openjdk/jdk7/promoted/b147/openjdk-7-fcs-src-b147-27_jun_2011.zip
MD5:c284c89a104f64a95afde3a96138ef0f
其他环境说明:
CentOS 7.4 64 位
- gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
- GNU Make 3.82
安装依赖
- yum -y install gcc gcc-c++ make
- yum -y install alsa-lib-devel
- yum -y install cups-devel
- yum -y install libX*
- yum -y install gcc gcc-c++
- yum -y install libstdc++-static
- wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo
- yum -y install ant
编译 JVM, 需要使用到更早之前一个版本的 JDK, 比如我们编译的是 7, 就需要安装 OracleJDK6:
下载地址: https://www.oracle.com/java/technologies/javase-java-archive-javase6-downloads.html
http://gcdncs.101.com/v0.1/static/test_mzb/jdk-6u38-linux-x64-rpm.bin
下载: jdk-6u38-Linux-x64-rpm.bin
- $ sh jdk-6u38-Linux-x64-rpm.bin
- $ sudo rpm -ivh jdk-6u38-Linux-amd64.rpm
编写编译脚本
解压 openjdk:
unzip openjdk-7-fcs-src-b147-27_jun_2011.zip
在 openjdk 目录下添加一个 build.sh 脚本:
- #!/bin/bash
- export LANG=C
- # 将一下两项设置为你的 BootstrapJDK 安装目录
- export ALT_BOOTDIR=/usr/java/jdk1.6.0_38
- export ALT_JDK_IMPORT_PATH=/usr/java/jdk1.6.0_38
- # 允许自动下载依赖包
- export ALLOW_DOWNLOADS=true
- # 使用预编译头文件, 以提升便以速度
- export USE_PRECOMPILED_HEADER=true
- # 要编译的内容, 我只选择了 LANGTOOLS,HOTSPOT 以及 JDK
- export BUILD_LANGTOOLS=true
- export BUILD_JAXP=false
- export BUILD_JAXWS=false
- export BUILD_CORBA=false
- export BUILD_HOSTPOT=true
- export BUILD_JDK=true
- # 要编译的版本
- export SKIP_DEBUG_BUILD=false
- export SKIP_FASTDEBUG_BUILD=true
- export DEBUG_NAME=debug
- # 避免 javaws 和浏览器 Java 插件等的 build
- BUILD_DEPLOY=false
- # 不 build 安装包
- BUILD_INSTALL=false
- # 包含全部的调试信息
- export ENABLE_FULL_DEBUG_SYMBOLS=1
- # 调试信息是否压缩, 如果配置为1,libjvm.debuginfo 会被压缩成 libjvm.diz, 将不能被 debug.
- export ZIP_DEBUGINFO_FILES=0
- # 用于编译线程数
- export HOTSPOT_BUILD_JOBS=3
- # 设置存放编译结果的目录
- #export ALT_OUTPUTDIR=/root/jvm/output
- unset CLASSPATH
- unset JAVA_HOME
- make sanity
- DEBUG_BINARIES=true make 2>&1
然后执行 sh build.sh 进行编译. 如果编译过程中遇到问题, 可以查阅下文的编译问题解决的部分.
运行 HotSpot
编译成功后, 编译的输出默认在 openjdk/build 目录下. HotSpot 的编译输出在 openjdk/build/Linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg 目录下.
使用 HotSpot 提供的命令执行测试:
- cd build/Linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg
- # test_gamma 是 HotSpot 提供的一个测试程序, 可以成功执行说明编译成功
- ./test_gamma
- # 使用 hotspot 脚本进行 GDB 调试
- ./hotspot -gdb HelloWorld
./hotspot 是一个脚本, 查阅代码可以看出他做了一些简单的事情, 主要是会设置环境变量:
- JAVA_HOME=/usr/java/jdk1.6.0_38
- LD_LIBRARY_PATH=/root/jvm/openjdk/build/Linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg:/usr/java/jdk1.6.0_38/jre/lib/amd64
然后生成 GDB 参数脚本, 并运行 GDB 命令:
gdb -x /tmp/hsl.26037
/tmp/hsl.26037 是脚本生成的 gdb 参数, 我们可以看看都设置了什么:
- cd /root/jvm/openjdk/build/Linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg
- handle SIGUSR1 nostop noprint
- handle SIGUSR2 nostop noprint
- set args HelloWorld
- file /root/jvm/openjdk/build/Linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/gamma
- directory /root/jvm/openjdk/build/Linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg
- # Get us to a point where we can set breakpoints in libjvm.so
- break InitializeJVM
- run
- # Stop in InitializeJVM
- delete 1
- # We can now set breakpoints wherever we like
可以看出设置了源码目录, 设置了一个默认断点. 分析了 hotspot 脚本后, 我们可以根据需要用最原始的方式来启动 hotspot 和 gdb 来实现更复杂的调试需求, 比如远程调试.
直接执行的方式:
JAVA_HOME=/usr/java/jdk1.6.0_38 LD_LIBRARY_PATH="/root/jvm/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg:/usr/java/jdk1.6.0_38/jre/lib/amd64" ./gamma HelloWorld
GDB 调试:
JAVA_HOME=/usr/java/jdk1.6.0_38 LD_LIBRARY_PATH="/root/jvm/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg:/usr/java/jdk1.6.0_38/jre/lib/amd64" gdb ./gamma HelloWorld
GDB 远程调试:
JAVA_HOME=/usr/java/jdk1.6.0_38 LD_LIBRARY_PATH="/root/jvm/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg:/usr/java/jdk1.6.0_38/jre/lib/amd64" gdbserver :8011 ./gamma HelloWorld
Hotspot 代码调试技巧
GDB 的使用和技巧这里就不说了, 我自己也是遇到问题现查资料的, 这里列几个常用的和 HotSpot 有关的调试技巧.
如何打印 HotSpot 内部符号对象 Symbol 对应的字符串?
Symbol 是一个非常常见的类, 所有的符号引用对应的字符串, 都会用 Symbol 来表示, 比如类名, 方法名, 方法签名等等, 可以用一下方法输出 Symbol 对应字符串:
p *name._body@name._length
如何打印 KlassHandle 对应的类名?
p Klass::cast(current_klass.obj())->external_name()
添加加载特定类时的断点
- break ClassFileParser::parseClassFile if strncmp(class_name._body, "XXX", 3) == 0
- break SystemDictionary::load_instance_class if strncmp(class_name._body, "XXX", 3) == 0
遇到的问题
BootstrapJDK 一开始设置为 JDK8 会失败, 要改为 JDK6
- # 错误
- echo "*** This OS is not supported:" `uname -a`; exit 1;
- # 解决
- sudo VIM openjdk/hotspot/make/Linux/Makefile
注释掉以下三行
- 238 #ifeq ($(DISABLE_HOTSPOT_OS_VERSION_CHECK)$(EMPTY_IF_NOT_SUPPORTED),)
- 239 # $(QUIETLY)>&2 echo "*** This OS is not supported:" `uname -a`; exit 1;
- 240 #endif
- # 错误
- error:"__LEAF"redefined [-Werror]
- # 解决
ubuntu12 的 glibc 比较新, 在 Linux 的头文件 cdefs.h 里, 有个__LEAF 的宏,
这个和 hotspot/src/share/vm/runtime/interfaceSupport.hpp
这个头文件中的宏定义有冲突, 我们在 428 行下面增加一个 #undef __LEAF 如下:
- 428 // 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
- # 错误
- Error:/openjdk/hotspot/src/share/vm/oops/constantPoolOop.cpp:272:39:
- error: converting 'false' to pointer type 'methodOop' [-Werror=conversion-null]
- # 解决
- vi hotspot/src/share/vm/oops/constantPoolOop.cpp
将 272 行 return false 改为 return NULL
- # 错误
- /usr/openjdk/hotspot/src/share/vm/opto/loopnode.cpp:896:49:
- error: converting 'false' to pointer type 'Node*' [-Werror=conversion-null]
- # 解决
- vi hotspot/src/share/vm/opto/loopnode.cpp
将 896 行 return false 改为 return NULL
- # 错误
- Using java runtime at: /usr/lib/jvm/java-1.6.0/jre
- ./gamma: relocation error: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.41.x86_64/jre/lib/amd64/libjava.so: symbol JVM_FindClassFromCaller, version SUNWprivate_1.1 not defined in file libjvm.so with link time reference
- # 解决
这里是有一个坑的, 为了避免大家踩坑, 请提前安装好 Oracle JDK 1.6.
- # ALT_BOOTDIR 用到的是 OpenJDK 1.6.0 会有此报错, OpenJDK 的 bug, 需要使用 Oracle JDK
- # 见到类似下方的报错了, 恭喜童鞋您入坑了
- # ./gamma: relocation error: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.41.x86_64/jre/lib/amd64/libjava.so: symbol JVM_FindClassFromCaller, version SUNWprivate_1.1 not defined in file libjvm.so with link time reference
下载传送门可能需要登陆 Oracle, 没有帐号的童鞋请注册一下. 笔者下载安装的是 jdk-6u38-Linux-x64-rpm.bin.
如果上一个传送门失效, 请继续传送! 找到此页面上的 Java SE 6 进入传送哦~如果这个传送也失效了 (T_T), 那接着传送, 拉到页面最下方, 找到 Java Archive 栏, 点击右侧 DOWNLOAD 按钮自行传送. 再不行就只能找 baidu 了~~~
- # 对下载到的 bin 动动手脚 (不要想多, 释放里面的 rpm 包而已)
- $ sh jdk-6u38-Linux-x64-rpm.bin
- # 查看下得到的 rpm 包
- $ ll *.rpm
- # 安装 Oracle JDK
- $ sudo rpm -ivh jdk-6u38-Linux-amd64.rpm
- # OK 至此已完成 Oracle JDK 安装
- # 查找安装的 Oracle JDK 目录
- # 查找 jdk 安装名称
- $ rpm -qa | grep ^jdk-1.6.0
- jdk-1.6.0_38-fcs.x86_64
- # 根据安装名称查找安装到本地的文件列表
- $ rpm -ql jdk-1.6.0_38-fcs.x86_64
- ...
- /usr/java/jdk1.6.0_38 # Oracle JDK HOME
- ...
- # 以上查找到的目录后面会用到
错误:
gcc: error: unrecognized command line option '-mimpure-text'
解决:
vi jdk/make/common/shared/Compiler-gcc.gmk
在 70 行 remove the command "-mimpure-text" in the code:
错误:
Error: time is more than 10 years from present: 1136059200000
解决:
- # 修改以下文件, 将日期改为十年以内, JDK 的 Bug.
- vi jdk/src/share/classes/java/util/CurrencyData.properties
- # line: 108 377 439 529 555
错误:
../../../src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java:661: error: no suitable constructor found for SslRMIServerSocketFactory(SSLContext,String[],String[],boolean)
解决:
vi ./jdk/src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java
注释掉 662 行的参数
资料
《深入理解 Java 虚拟机》
openjdk7 之编译和 debug | YDDMAX https://yddmax.github.io/2017/06/11/openjdk7之编译和debug/
centos7 编译 openjdk7 常见问题 - clover 灬 - OSCHINA https://my.oschina.net/zhangdq/blog/2250314
How to build and package OpenJDK 7 on Linux . hgomez/obuildfactory Wiki https://github.com/hgomez/obuildfactory/wiki/How-to-build-and-package-OpenJDK-7-on-Linux
ubuntu16.04 编译 JDK7 - 简书 https://www.jianshu.com/p/32dc1a850e23
Building OpenJDK7 with CentOS7 - Rtfsc8 http://blog.rtfsc8.top/2018/07/07/building-openjdk7-with-centos7/
CentOS 上编译 OpenJDK8 源码 以及 在 eclipse 上调试 HotSpot 虚拟机源码 - tjiyu 的博客 - CSDN 博客
本文独立博客地址: JVM 源码分析 - JVM 源码编译与调试 | 木杉的博客
来源: https://www.cnblogs.com/mushan/p/12266446.html