mac下openjdk12源码阅读调试环境搭建

2,019 阅读16分钟

前言

自菜鱼接触Java语言的一开始,就听说Java是一门解释性语言,至于啥是解释性语言,咱也不知道。只知道把业务代码编写进.java文件,然后由IDE编译成一堆.class文件,最后打包成一个.jar文件,扔到服务器上执行不就得了嘛,至于Java是编译性语言还是解释性语言,有什么关系呢?运行中出了问题,再解决嘛,菜鱼我就是想上班摸鱼,下班后骑着我那辆心爱的一手山地车以车轮丈量秦淮河,这才是惬意的生活啊。

话虽然这样说,但是架不住疯狂而且无休止的内卷啊。今天问你ArrayListLinkedList的区别,明天问你HashMap源码,后天问你Thread。毕竟这还只是JDK源码层面的卷,毕竟是靠Java吃饭的,这本无可厚非。直到有一天,卷到了volatilesynchronized这些关键字上面,更别说Java的垃圾回收器了,这些都是卷的重灾区,卷来卷去,最终卷到了Java的执行机制上面了。

我本将心向明月,奈何明月照沟渠。

准备

既然准备调试JVM了,那就先泼一盆冷水吧。整个JVM是由少量C和大大大量C++编写的,没有一些C/C++功底,比如基础的语法,再高级一点指针,如果搞不清楚多级指针、函数指针这些概念,那劝你还是省省吧。有这个时间多看看面试题,刷刷算法题不行嘛,哪怕打一局农药呢,干嘛花时间遭这个罪。还有,就算你把源码调试环境搞定了,然后结果就是把源码调试搞定了,三分钟热度一过,又被其他新潮技术吸引走了。

不过,要是想编译自己的JDK,还是可以的。举个最简单的例子,我们在工作期间,想看HashMap的源码,打开源码看到一堆英文注释,这时候如果想把自己的理解用中文写进源码里面,这就需要编译自己的JDK了,看不懂C/C++,但是可以看懂Java啊。具体还是看个人抉择吧。

菜鱼我为什么选择OpenJDK12呢?一开始的时候,菜鱼我选择的是OpenJDK8,编译什么的都通过了,唯独在JDK源码里面写中文注释搞不定,真的是花了很大的力气,但菜鱼就是菜鱼,真的没搞定。后来选择了OpenJDK11,这个版本成功了。研究了很长时间,刚刚触摸到门槛,因为个人电脑升级了,就顺便把版本换成了OpenJDK12。再往后的版本就没追了,但尝试编译过,都成功了。

下载源码

菜鱼我不建议去git上直接把源码clone下来,原因很简单,这是一个大工程,你看源码的速度永远跟不上更新的速度,还不如直接就下载一个发行版的源码,自己编译玩玩呢。jdk.java.net/ 是OpenJDK各个发行版的入口

Screen Shot 2022-01-09 at 15.48.33.png

选择一个版本,点击进入后,下图红框里面的红框,就是整套源码,直接点击下载就OK了。

Screen Shot 2022-01-09 at 15.50.18.png

构建环境

源码解压后,在根目录下面有一个doc目录,进去之后,里面有一个building.html文件,里面详细的记载了构建所需的环境和工具。有些人不推荐在Windows下搞这东西,其实菜鱼我也不推荐,接下来,菜鱼介绍一下菜鱼我自己的艰难历程。

一开始菜鱼我是在Windows系统里面搞了一个Windows虚拟机,为的是避免搞废之后连累物理机。按照building.html里面的介绍,安装了Cygwin,在安装Cygwin的过程中,顺便安装了makeautoconfzipunzip这几个组件。最后安装了visual studio 2015,这个IDE就是Windows下的VC++那一套。最后在Cygwin提供的命令行里面,执行bash configure ..。,配置完毕以后,再执行make images,这样就能构建出我们自己的JVM和JDK了。简单了几句话,菜鱼我曾经硬生生的搞了两天,虚拟机重装了三次。而且,最后退而求其次,在物理机中使用Source Insight这个工具阅读源码,然后推到虚拟机中编译,编译完成后,再把JDK复制到物理机中,靠着日志去调试。整个过程很繁琐,而且编译一次要花费很长时间,渐渐的就忘记了。除了菜鱼我无法去debug之外,菜鱼我没有把在Windows下搭建环境的过程记录下来。so,现在,菜鱼我手中只保留了那个Windows编译环境虚拟机,有兴趣可以找菜鱼哦。

偶然的原因,菜鱼我换了一台mac,又重新拾起了这套源码,接下来就是记录菜鱼我在mac下构建OpenJDK的过程。

JVM是C/C++编写的,必须要有支持这个语言的编译器,Linux下使用的是gcc/g++,Windwos下面就是VC++那一套,mac下就是直接下载Xcode,帮你搞定一切。在构建之前,还需要配置一个Boot JDK,C/C++用来构建JVM,那Java编写的JDK谁来构建?靠的就是这个Boot JDK,我们不考虑先有鸡还是先有蛋的问题,准备好这个环境就OK了。另外,这个Boot JDK还有另外一个要求,Boot JDK版本必须比你要构建的版本低。这些在building.html中都有描述,菜鱼我也不再过多叙述。

构建与调试

配置与构建

进入到源码根目录,运行bash configure,开始构建之前的配置与检查。比如,检查是否存在Makefile工具,是否存在Boot JDK,以及Boot JDK的版本是否符合规则等。顺便啰嗦一句吧,在Linux环境下,源码安装一些工具,诸如MySQL,nginx在构建之前都需要运行配置检查,而且在它们的源码下面,都会有一个类似的configure文件。至于命令的前缀bash,这是shell的一个解释器,默认使用的是bash,除却这个之外,还有比如shzsh等,不再过多啰嗦了。

在运行配置检查的时候,菜鱼我这里出现了一个问题:

Screen Shot 2022-01-09 at 17.04.58.png

这是在说什么呢?这是在说菜鱼我本机的Boot JDK版本有问题,使用的是OpenJDK13,但是需要的是OpenJDK11或是OpenJDK12,可以使用--with-boot-jdk这个参数来指定Boot JDK。其实就是菜鱼我本机上安装了多个版本的JDK,由于没有指定使用什么版本,就造成了这种错误。那简单,指定一下不就OK了嘛。重新运行:

bash configure --with-boot-jdk='/Users/opensoftware/custome-jdk-11'

配置检查完成,屏幕上就会出现下面的日志


====================================================
A new configuration has been successfully created in
/Users/xxx/openjdk/build/macosx-x86_64-server-release
using configure arguments '--with-boot-jdk=/Users/opensoftware/custome-jdk-11'.

Configuration summary:
* Debug level:    release
* HS debug level: product
* JVM variants:   server
* JVM features:   server: 'aot cds cmsgc compiler1 compiler2 dtrace epsilongc g1gc graal jfr jni-check jvmci jvmti management nmt parallelgc serialgc services shenandoahgc vm-structs'
* OpenJDK target: OS: macosx, CPU architecture: x86, address length: 64
* Version string: 12-internal+0-adhoc.username.openjdk (12-internal)

Tools summary:
* Boot JDK:       openjdk version "11-internal" 2018-09-25 OpenJDK Runtime Environment (build 11-internal+0-adhoc.username.openjdk11) OpenJDK 64-Bit Server VM (build 11-internal+0-adhoc.username.openjdk11, mixed mode)  (at /Users/opensoftware/custome-jdk-11)
* Toolchain:      clang (clang/LLVM from Xcode 12.5.1)
* C Compiler:     Version 12.0.5 (at /usr/bin/clang)
* C++ Compiler:   Version 12.0.5 (at /usr/bin/clang++)

Build performance summary:
* Cores to use:   16
* Memory limit:   16384 MB

大意是,已经在/Users/xxx/openjdk/build/macosx-x86_64-server-release这个目录下面创建了一个新的配置,使用的配置参数是'--with-boot-jdk=/Users/opensoftware/custome-jdk-11',下面一堆配置指标,值得关注的是Debug level: release也就是说,我们这次配置的是release版,关闭了一些调试信息。 现在,开始构建JVM和JDK,命令也很简单:

make images

然后又出问题了:

Screen Shot 2022-01-09 at 18.25.44.png

这段完整的代码是这样的:

// src/hotspot/share/runtime/sharedRuntime.cpp:2873:85
double locs_buf[20];
buffer.insts()->initialize_shared_locs((relocInfo*)locs_buf, sizeof(locs_buf) / sizeof(relocInfo));

sizeof是C语言中计算变量大小的一个操作符(关键字),不是一个函数,类似于Java中synchronized(obj)这种语法。这个问题产生的原因这里先按下不表,只需要知道,这在C++中是一个警告。由于严格的检查机制,警告转变成了错误,这是OpenJDK内部决定的。为了能继续构建,只有一个解决办法,把警告压制下去,在C++中,常用的警告压制就是在代码中使用#program warning(disable xxx),但是现在还不行,我们还没有修改源码的能力,你能修改一处,就还有十处等着你。别问菜鱼为啥知道,因为菜鱼改代码直接改崩溃了(这个问题在Windows中也存在,所以菜鱼就很好奇,OpenJDK的开发人员是怎么解决的?)。

configure中有没有有一个配置参数,能直接压制警告呢?运行bash configure --help,查看帮助: Screen Shot 2022-01-09 at 19.07.24.png

帮助文档中确有这个类型的参数,而且默认是启用的,也就是会把构建期间的警告转变成错误。那就简单了,运行配置的时候,把这个参数加上不就OK了,之前还有Boot JDK的错误,参数也别忘记喽。重新运行bash configure

bash configure --with-boot-jdk='/Users/opensoftware/custome-jdk-11' --disable-warnings-as-errors

再次make images,如果电脑性能不好,这个过程会很慢,很慢。构建成功后,会显示下面的提示。

Screen Shot 2022-01-09 at 19.20.39.png 构建完成以后,在源码的根目录下,有一个build/os-bits-variant-debuglevel文件夹,在运行配置的时候就已经生成,当构建完成以后,生成的JDK就存放在这个目录里面。os-bits-variant-debuglevel是根据操作系统、CPU架构、JVM variants(即server,client,minimal...)和debug level生成的文件夹,更多的内容,还是去看building.htmlbash configure --help,下面就是菜鱼我本机的目录以及生成的JDK: Screen Shot 2022-01-09 at 19.29.17.png

进入到jdk目录里面,运行bin/java -version

Screen Shot 2022-01-09 at 19.39.37.png

至此,你已经成功的构建出了自己的JDK,已经完成了第一阶段,恭喜你,你现在就可以把jdk放到自己的环境变量中,以后开发就可以使用自己的JDK喽。

魔改JDK源码

听人家说,在Java1.8中,HashMap里面当同一个Key的冲突链达到8个节点时,整条冲突链就会转成红黑树,只是听人家说,但是菜鱼我没看到啊,当冲突链开始转化成红黑树的时候,能不能给菜鱼我一个提示呢。又听人家说,冲突链之所以达到8个节点转换成红黑树,原理是一个叫做泊松分布的理论在支持,像这种只有卷王之王才能知道的顶级功法,菜鱼我肯定想第一时间就把这就话写在HashMap的源码中,每次跳进HashMap源码的时候,都能看到这句话,天天顶礼膜拜。

现在已经我们已经构建出了自己的JDK,不就是一句话嘛,找到HashMap源码,直接加进去。就像下面这样:

Screen Shot 2022-01-09 at 20.06.53.png

保存,回到源码根目录,重新构建,运行make images,然后就出现了下面这种情况: Screen Shot 2022-01-09 at 20.09.15.png 这什么情况?简单来说,咱们的中文不是ascii字符呗。那就好解决了,找到编译.java文件的地方,在java命令后面增加一个-encoding utf-8,事情就OK了。去根目录下的make目录,打开CompileJavaModules.gmk文件,找到第41行,现在是这样:

Screen Shot 2022-01-09 at 20.25.29.png

把编码添加进去,就成了这样:

Screen Shot 2022-01-09 at 20.27.08.png 保存,回到源码根目录,重新构建,运行make images

首先,菜鱼我要说的是,只改这个一个地方,只能对java.base这个模块起作用,其他的诸如java.sqljava.instrument等模块根本起不到半点作用。修改的地方已经不止涉及CompileJavaModules.gmk这一个文件了,但是这个文件中还是有很多地方是能起到作用的,其格式为:java.xxx_ADD_JAVAC_FLAGS +=。在这个格式后面添加上-encoding utf-8,就能满足愿望了。

写到这,是不是觉得菜鱼我怎么知道这些东西的?如果你编写过Makefile,自己就知道去哪里找了。唉,屠龙者终成恶龙啊!!!

调试JVM之前的准备

这一小节主要讲述的是菜鱼我在搭建调试JVM环境中遇到的一些问题,涉及到的是C/C++的知识点,如果只是想玩玩JDK,这一节可以跳过去了。菜鱼我使用的IDE是CLion,不止调试JVM,调试Redis使用的也是这个工具。现在就要说明一下了,JVM的构建工具是make,而CLion支持的是cmake,所以我们需要把make项目转成cmake项目。这就需要借助一个工具compiledb,这是一个Python项目,如果已经安装了pip,直接运行sudo pip install compiledb,安装完成后,执行compiledb -help来查看是否安装完毕。

安装完毕以后,就要开始重新编译了。回到源码根目录,运行:

make CONF=macosx-x86_64-server-release compile-commands
make CONF=macosx-x86_64-server-release

Screen Shot 2022-01-09 at 23.09.32.png

然后进入build/macosx-x86_64-server-release目录,里面有一个compile_commands.json文件,这就是这次命令执行生成的文件:

Screen Shot 2022-01-10 at 21.45.09.png

现在准备导入,导入之前,先打开CLion,配置一条Toolchains: Screen Shot 2022-01-10 at 19.37.06.png 配置结果如下:

Screen Shot 2022-01-10 at 19.38.41.png

红框里面的Name一定要配置成build目录下面的那个os-bits-variant-debuglevel,菜鱼我这里是macosx-x86_64-server-release。至于下面的MakeC CompilerC++ Compiler按照默认的来吧,这里需要注意的一点是最下面的Debugger,菜鱼我使用的是LLDB,而且接下来调试出现的问题也是基于LLDB来解决的。确认之后,回到CLion的主界面,选择New CMake Project from Sources

Screen Shot 2022-01-10 at 20.01.06.png 然后找到你compile_commands.json文件所在的目录,选择打开:

Screen Shot 2022-01-10 at 19.49.42.png

点击确认之后,跳出一个弹窗,直接点击确认,就OK了。进来之后,你能在调试栏发现一个: Screen Shot 2022-01-10 at 19.51.44.png 看到名称了吧,如果你的Toolchains配置的名称不符合上述规范,那只能干瞪眼了(删掉Toolchains,重新配置,重新导入)。现在的目录是在你的build下面,需要重新设置一下CLion中的根目录,选择Tools->CMake->Change Project Root

Screen Shot 2022-01-10 at 19.55.10.png 在弹窗里选择你自己的OpenJDK源码根目录:

Screen Shot 2022-01-10 at 19.57.52.png 最后Tools->CMake->Reload CMake Project

Screen Shot 2022-01-10 at 20.02.57.png 至此,源码阅读环境搭建成功。

开始JVM调试

现在搭建的环境,是一个伪CMake项目,还有一些东西需要配置。思考一下,你调试JVM的目的是什么?是想知道java命令的执行过程,对吧。那么现在的配置是不足以支撑你的需求,因为我们构建JVM生成了很多可执行命令,除了java之外,还有jarjavadoc等等,不一而足。而且,你要调试java,总得有个.java文件来让你爽一爽吧,现在就开始干活。先编写一个.java文件:

public class Main{
    public static void main(String[] args){
        System.out.println("Hello World!!!!!!!!");
    }
}

这个文件想存放在什么位置都可以。现在开始debug进行的配置,点击:

Screen Shot 2022-01-10 at 20.15.36.png

选择Edit Configurations...

Screen Shot 2022-01-10 at 20.16.51.png

需要注意的是ExecutableProgram argumentsBefore launch,这里Executable里面的内容需要配置成你编译完成的java,就在build/os-bits-variant-debuglevel目录下,找jdk/bin目录里面的java命令;Program arguments就配置成刚才编写的.java文件的全路径名。最后,如果你不懂C/C++里面的编译机制,把Before launch里面的Build删除掉。 Screen Shot 2022-01-10 at 20.30.38.png

至此,点击调试栏里面那个可爱的小爬虫,就能愉快的玩耍了。但是问题总在不经意间就出来了,我们一个断点没打,但是程序却总是自己进入断点,比如这种:

Screen Shot 2022-01-10 at 20.39.11.png 还有这种:

Screen Shot 2022-01-10 at 20.39.33.png

特别是StubRoutines::call_stub(),这个调用非常,非常,非常的重要,这是整个Java体系中,函数调用的入口点,你的main方法,你的普通方法,你的静态方法几乎所有的方法调用,入口点都在这里。当然,这篇博文的要点不是这里,继续说我们的问题。自动进入断点以后,你直接点击那个小三角号放过去就行了:

Screen Shot 2022-01-10 at 20.45.05.png

最后,在控制台,能看到我们编写的.java文件打印的信息:

Screen Shot 2022-01-10 at 20.47.50.png

现在解决那个自动断点的问题,首先声明一下,菜鱼我对这个问题产生的原因知道一丢丢,但不敢保证正确,这里就不献丑了。菜鱼所知道的解决方案,也是从别的地方抄袭来的,外加自己一点一点的探索和摸索。解决问题的第一步,在你的源码根目录下面,新建一个.lldbinit文件,把这段内容设置进去:

br set -n main -o true -G true -C "pro hand -p true -s false SIGSEGV SIGBUS"

第二步,回到你的用户目录下,也新建一个.lldbinit文件,把这段内容设置进入:

settings set target.load-cwd-lldbinit true

然后重新debug,问题已解决,后面就自己慢慢摸索吧。

OpenJDK环境搭建后记

花费了两天时间,终于写完了。菜鱼我毕竟还是一条菜到抠脚的咸鱼,很多问题也是借助于其他人的文章才完成的,这里推荐一些个人的参考文章吧:

虽然搭建完毕了,但是在阅读的过程中,肯定还会有些问题。比如对C/C++的语法不熟悉、对运行的机制不熟悉、找不到main函数的入口和令人恶心的大段宏定义等等诸多问题。这些问题菜鱼我以后会陆续写一些文章来完善吧,共同学习嘛。由于每个人的环境不一样,所产生的问题也不一样,比如菜鱼我之前在Windows上构建环境,就修改过visual studio中的配置,还有各种恶心的依赖包。

即使到了这一步,菜鱼我还是要再泼一盆冷水,研究JVM源码,势必要学习C/C++,而且还不是皮毛的那种,虽然这堆源码里面没有用到C++里面最新的语法,但是这对我们普通人而言也是一个挑战。除了要学习C/C++之外,还要对操作系统以及CPU指令集那一堆东西要有一些研究,顺便还要学习一下汇编语言。还要有看一手资料的能力,哪怕借助翻译软件,也要培养出来这个能力。说了这么多,都是菜鱼我在研究JVM源码期间所遇到的坎坷。如果你就此放弃,也没啥,能编译自己的JDK,在自己的JDK中写上自己的见解和注释,也很不错了。

后面菜鱼我会陆续翻译几篇关于JVM内部运行机制的文章,有时间发布一下。