如何开始Java虚拟机的学习?
因为有时候排查问题如果更深层次, 不可避免的需要从虚拟机源码入手。但很多同学看到要调试虚拟机或者看虚拟机源码就已经心理产生了退缩情绪。而学习虚拟机最好的方式就是自己尝试build出一个可用的JDK, 然后在这个JDK上做各种修改和调试已达到自己的期望。本文从我当时初学的一些文档(仅限MacOS环境)整理,希望能对大家有帮助。
概念定义
Mercurial是一种轻量级分布式版本控制系统,采用 Python 语言实现,易于学习和使用,扩展性强。其是基于 GNU General Public License (GPL) 授权的开源项目。
在早期需要获取JDK以及Hotspot的源代码时,基本上都会推荐去openjdk官网,通过Mercurial去克隆。
比如想要下载JDK9的源码包
hg clone http://hg.openjdk.java.net/jdk9/jdk9
sh get_source.sh
使用Mercurial从openjdk去下载源码。这是万事的第一步。但是这里就遇到了问题。下载速度实在是太慢了。家庭网络速度下载大概在5kb/s。公司内网下载速度能到100kb/s, 但是经常会出现莫名其妙的错误。
你可能会看到这样的报错:
jaxp: transaction abort!
jaxp: rollback completed
jaxp: abort: stream ended unexpectedly (got 33981 bytes, expected 44934)
或者这样:
jaxws: transaction abort!
jaxws: rollback completed
jaxws: abort: HTTP request error (incomplete response) jaxws: (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator)
https://github.com/unofficial-openjdk/openjdk/
https://github.com/dmlloyd/openjdk
当然git获取也有可能失败(但出问题的概率远小于Mercurial)。那只能多尝试几次碰碰运气了。
$ git clone https://github.com/dmlloyd/openjdk.git
Cloning into 'openjdk'...
remote: Counting objects: 1205936, done.
remote: Compressing objects: 100% (4192/4192), done.
error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno
54
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
$ git clone https://github.com/dmlloyd/openjdk.git
Cloning into 'openjdk'...
remote: Counting objects: 1205936, done.
remote: Compressing objects: 100% (4192/4192), done.
error: RPC failed; curl 18 transfer closed with outstanding read data remaining
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
$ git clone https://github.com/dmlloyd/openjdk.git
Cloning into 'openjdk'...
remote: Counting objects: 1205936, done.
remote: Compressing objects: 100% (4192/4192), done.
error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 60
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
必备组件
FreeType
FreeType是一个用C语言实现的一个字体光栅化库。它可以用来将字符栅格化并映射成位图以及提供其他字体相关业务的支持。
X11
https://www.xquartz.org/
和UI界面有关, 比如在初学Java的时候, 我们接触过的JWT.
XCode准备
XCode版本
不要乱升级XCode。
对于非Apple Developer来说, XCode有个很坑爹的事情, 就是他升级到高版本后, 无法回退到低版本。如果要重新安装低版本, 需要Apple Developer账号去下载。而且XCode的安装包要5G, 解压后分分钟10个G(对于小硬盘容量的记起来说, 是个噩梦)。
XCode兼容性
XCode升级到10之后, 删除了底层目录下的libstdc++文件。导致在JDK在Make时会报错, 无法识别类似这样的C++语法。而在XCode9时对应文件还是存在的。官方给出的意思是libstdc++已经被标记为过期5年了, 现在统一使用自己libc++。这个问题最简单的问题就是XCode版本回退到9之前即可。
操作系统兼容性
OpenJDK的build的官方README说建议XCode版本为8.3.2。我按照要求回退到该版本, 却发现该版本和 Mojave(10.14) 不兼容。XCode打开一闪而过。同时在安装目录下发现了不兼容导致的crash文件。无奈重新把版本拉到XCode9。
Configure
sh configure
--with-debug-level=slowdebug
--disable-warnings-as-errors
--with-freetype-include=/usr/local/Cellar/freetype/2.9.1/include/freetype2
--with-freetype-lib=/usr/local/Cellar/freetype/2.9.1/lib
--with-boot-jdk=/Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/Home
--with-target-bits=64
这里稍微解释下几个参数:
--with-debug-level: 指定DEBUG级别。可选项有slowdebug, fastdebug, optimized, release(默认)。
--disable-warnings-as-errors: 不认为告警是错误
--with-boot-jdk: 指定bootjdk。你要编译一个JDK,必须本机已经安装了一个JDK来作一些引导启动工作。
一切顺利的话这里会看到如下的结果:
环境确认
OK, 经过上述准备工作后给出基本的编译环境(注意,这里只是我本机的环境,非强制):
◆ BOOT-JDK: 1.8.0.161-b12
◆ 操作系统: Mojave(10.14)
◆ XCode: 9
Make
make是一个在软件开发中所使用的工具程序(Utility software),经由读取“makefile”的文件以自动化建构软件。它是一种转化文件形式的工具,转换的目标称为“target”;与此同时,它也检查文件的依赖关系,如果需要的话,它会调用一些外部软件来完成任务。它的依赖关系检查系统非常简单,主要根据依赖文件的修改时间进行判断。大多数情况下,它被用来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。它使用叫做“makefile”的文件来确定一个target文件的依赖关系,然后把生成这个target的相关命令传给shell去执行。
当Configure完成后,此处只需在根目录简单的一条命令即可:
make all
Make成功的标志如下:
----- Build times -------
Start 2022-01-16 00:53:13
End 2022-01-16 01:00:44
00:07:31 TOTAL
Finished building target 'all' in configuration 'macosx-x86_64-normal-server-
slowdebug'
如何调试?
上述的Make阶段完全可以仅通过make hotspot来完成虚拟机部分的make,后续可通过gdb来完成调试。但对于部分新入门的同学来说,门槛显的稍微高了一点点。下边介绍两种相对简单的方式。
ostream组件
ostream可以将字符串输出到控制台。对于Java开发同学来说, 结果类似于System.out.println("xx"). 还记得我们刚开始学习Java的时候, 就是通过System.out.println来观察程序输出结果是否满足期望。在虚拟机里边刚开始学习的时候,依然可以这么做。
比如下方代码, 可以在safepoint发生polling_page时, 输出对应的字符串到控制台。
...
// 调试引入ostream
// --------------------------------------------------------------------------------------------------
// Implementation of Safepoint begin/end
static bool timeout_error_printed = false;
void SafepointSynchronize::handle_polling_page_exception(JavaThread *thread) {
assert(thread->is_Java_thread(), "polling reference encountered by VM thread");
assert(thread->thread_state() == _thread_in_Java, "should come from Java code");
assert(SafepointSynchronize::is_synchronizing(), "polling encountered outside safepoint synchronization");
if (ShowSafepointMsgs) {
tty->print("handle_polling_page_exception: ");
}
if (PrintSafepointStatistics) {
inc_page_trap_count();
}
...
}
XCode调试模式
新建空白command-line-tool工程, 删除xcode自带的文件
修改允许目标为(源码SRC)/build/macosx-x86_64-normal-server-slowdebug/jdk/bin/java
右击XCode工程,选择 "add files to project" 添加源码文件
好了,你现在可以在任意感兴趣的地方开始Debug了!
心急的同学肯定关心的是虚拟机如何开始启动的?启动以后都做了什么事情?那就把断点设置在如下地方:
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
jint result = 0;
__try {
// 核心入口
result = JNI_CreateJavaVM_inner(vm, penv, args);
} __except(topLevelExceptionFilter((_EXCEPTION_POINTERS*)_exception_info())) {
// Nothing to do.
}
return result;
}
在调试时候,我们会碰到较多的SIGSEGV信号中断,大部分情况下我们不用特别关注, 只需过滤掉即可。可以在XCode右下角的LLDB窗口设置:
(lldb) process handle SIGSEGV --stop=false
一些规约(Native方法命名规范)
规范映射
native 方法对应的 C 函数都需要以Java_为前缀,之后跟着完整的包名和方法名。由于 C 函数名不支持/字符,因此我们需要将/转换为_。
我们以类加载器为例:
// return null if not found
private native Class<?> findBootstrapClass(String name);
转换到虚拟机中的函数为:
JNIEXPORT jclass JNICALL
*env, jobject loader,
jstring classname)
{
char *clname;
jclass cls = 0;
char buf[128];
...
}
主动映射
主动映射不限制函数名称,即不用遵守上述规范。以Object对象为例, 通过RegisterNatives函数将methods[]注册后,hashCode对应的函数就是JVM_IHashCode了。
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
Hotspot源码目录说明
这份目录说明最早出自于虚拟机领域国内第一人rednaxelafx,感谢。需要注意的是,该目录说明只适用于JDK9之前的版本,JDK9之后目录结构变化较大,注意甄别。
├─agent Serviceability Agent的客户端实现
├─make 用来build出HotSpot的各种配置文件
├─src HotSpot VM的源代码
│ ├─cpu CPU相关代码(汇编器、模板解释器、ad文件、部分runtime函数在这里实现)
│ ├─os 操作系相关代码
│ ├─os_cpu 操作系统+CPU的组合相关的代码
│ └─share 平台无关的共通代码
│ ├─tools 工具
│ │ ├─hsdis 反汇编插件
│ │ ├─IdealGraphVisualizer 将server编译器的中间代码可视化的工具
│ │ ├─launcher 启动程序“java”
│ │ ├─LogCompilation 将-XX:+LogCompilation输出的日志(hotspot.log)整理成更容易阅读的格式的工具
│ │ └─ProjectCreator 生成Visual Studio的project文件的工具
│ └─vm HotSpot VM的核心代码
│ ├─adlc 平台描述文件(上面的cpu或os_cpu里的*.ad文件)的编译器
│ ├─asm 汇编器接口
│ ├─c1 client编译器(又称“C1”)
│ ├─ci 动态编译器的公共服务/从动态编译器到VM的接口
│ ├─classfile 类文件的处理(包括类加载和系统符号表等)
│ ├─code 动态生成的代码的管理
│ ├─compiler 从VM调用动态编译器的接口
│ ├─gc_implementation GC的实现
│ │ ├─concurrentMarkSweep Concurrent Mark Sweep GC的实现
│ │ ├─g1 Garbage-First GC的实现(不使用老的分代式GC框架)
│ │ ├─parallelScavenge ParallelScavenge GC的实现(server VM默认,不使用老的分代式GC框架)
│ │ ├─parNew ParNew GC的实现
│ │ └─shared GC的共通实现
│ ├─gc_interface GC的接口
│ ├─interpreter 解释器,包括“模板解释器”(官方版在用)和“C++解释器”(官方版不在用)
│ ├─libadt 一些抽象数据结构
│ ├─memory 内存管理相关(老的分代式GC框架也在这里)
│ ├─oops HotSpot VM的对象系统的实现
│ ├─opto server编译器(又称“C2”或“Opto”)
│ ├─prims HotSpot VM的对外接口,包括部分标准库的native部分和JVMTI实现
│ ├─runtime 运行时支持库(包括线程管理、编译器调度、锁、反射等)
│ ├─services 主要是用来支持JMX之类的管理功能的接口
│ ├─shark 基于LLVM的JIT编译器(官方版里没有使用)
│ └─utilities 一些基本的工具类
└─test 单元测试
一些建议
关于断言/调试代码
在虚拟机源码中会出现大量的断言代码。比如:
// frame::sender_for_compiled_frame
frame frame::sender_for_compiled_frame(RegisterMap* map) const {
assert(map != NULL, "map must be set");
....
}
以及非正式环境下的各种调试代码。比如:
void InstanceKlass::print_dependent_nmethods(bool verbose) {
dependencies().print_dependent_nmethods(verbose);
}
bool InstanceKlass::is_dependent_nmethod(nmethod* nm) {
return dependencies().is_dependent_nmethod(nm);
}
对于这两种情况下的代码内容,个人给出的建议是可稍微了解,但在梳理整个链路时,不必过多关注其内容。
操作系统环境
Java的一次编译, 到处运行被奉为美谈。但到具体的操作系统,内部必然有和平台相关的代码。该动作会由虚拟机内部来透明路由完成,上层业务无需关心。以线程处理为例:
- thread_linux_86.cpp
- thread_bsd_86.cpp
- thread_solaris_86.cpp
- thread_windows_86.cpp
再以方法调用时的栈帧模型来举例:
- frame_x86.cpp
- frame_arm.cpp
- frame_s390.cpp
- frame_ppc.cpp
可以看到同一模型下有多重不同的实现。此处我们只用关心自己的平台即可。比如MacOS暂时只用关心操作系统为bsd, 架构为X86即可。
关于最新的MacOS(M1)
之前提到过XCode的兼容性问题,建议使用XCode9.4来进行调试。但在最新款的MacOS中, XCode9.4如果强行安装会提示版本过低,不建议使用。但如果使用高版本的XCode,又会有兼容性问题。同时采用ARM架构的Mac芯片M1,不具有学习上的通用性,各位慎重选择。
善于利用IDE的全文搜索功能
由于个人对于C++相关的IDE并不熟悉。所以在调试过程中大量使用了全文搜索功能。包括但不限于:
◆ 查找具体内部方法调用
◆ 查找Native方法的内部实现
◆ 查找虚拟机配置项
收藏那些精妙的代码
Hotspot中还是有很多代码值得学习。给我留下印象最深的是一些位操作的代码。举例如下:
http://hg.openjdk.java.net/jdk-updates/jdk9u/hotspot/file/22d7a88dbe78/src/share/vm/utilities/globalDefinitions.hpp#l484
// 这里是向上对齐的宏。比如size是20. 对齐大小alignment是8, 则尝试对齐到24=8*3.
// 这里是向下对齐的宏。比如size是20. 对齐大小alignment是8, 则尝试对齐到16=8*2.
对象容纳个数
http://hg.openjdk.java.net/jdk-updates/jdk9u/hotspot/file/22d7a88dbe78/src/share/vm/oops/typeArrayOop.hpp#l156
// 这里偷了个巧。LogHeapWordSize在64位下默认是3, 这里等于是除以8. 然后通过左边添加(HeapWordSize-1)来保证除后的HeapWord个数刚好能包住bytes。
julong size_in_words = ((size_in_bytes + (HeapWordSize-1)) >> LogHeapWordSize);
内存对齐
http://hg.openjdk.java.net/jdk-updates/jdk9u/hotspot/file/22d7a88dbe78/src/share/vm/memory/allocation.hpp#l42
「作者简介」
盈超
2018年加入网易严选,目前在职严选交易技术组,对于应用系统的疑难杂症处理有丰富经验,对Java虚拟机领域有浓厚兴趣。
关注 分享 在看,就是所长最大的生产力!