1. 程式人生 > >JVM原始碼分析-JVM原始碼編譯與除錯

JVM原始碼分析-JVM原始碼編譯與除錯

要分析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%E4%B9%8B%E7%BC%96%E8%AF%91%E5%92%8Cdebug/
  • 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原始碼編譯與除錯 | 木杉的部落格