目录
一、下载源码
二、编译
三、搭建开发环境
四、GDB调试
五、目录结构说明
可选择在OpenJDK(http://hg.openjdk.java.net/jdk8u)的官方代码仓库下载zip包,如下图所示:
可选择命令行的方式,先执行sudo yum install Mercurial 安装类似git的仓库管理工具hg,然后执行hg clone http://hg.openjdk.java.net/jdk8u/jdk8u命令下载源代码中最外层的目录,最后执行sudo sh get_source.sh下载所有子目录如上图jdk,hotspot下的代码。
这两种方式都是直接访问位于国外的官网,没有国内镜像,网速相当不稳定,容易下载失败。笔者后面在Githup上搜到一个由AdoptOpenJDK社区维护的镜像仓库(https://github.com/AdoptOpenJDK/openjdk-jdk8u),果断fork到自己的Github账号下,从Github上下载源码就快很多了,如下图:
AdoptOpenJDK提供了一个一键构建工具openjdk-build(https://github.com/AdoptOpenJDK/openjdk-build),官网的介绍是执行一条命令./makejdk-any-platform.sh jdk8u 即可完成编译,笔者因为公司网络管制无法在Linux虚机上访问Github,有兴趣的同学可尝试。
参考:OpenJDK8源码编译(Ubuntu 16.04)
openJDK之如何下载各个版本的openJDK源码
在windows上编译强依赖微软的Visual Studio的C/C++编译器,执行./configue时会根据当前操作系统类型强校验编译器类型,支持的编译器如下图:
本身没有安装Visual Studio的都推荐在Linux上编译。具体步骤如下:
1、确保已安装1.7的JDK,OpenJDK源码中Java API相关的Java代码需要借助已安装的JDK编译。OpenJDK7需要通过Ant脚本构建,还需要安装Ant相关依赖并配置环境变量,OpenJDK8改成经典的C编译方式,更加方便快捷。
2、sudo unzip openjdk-jdk8u-master.zip,将源码zip包解压
3、sudo sh ./configure --with-target-bits=64 --with-boot-jdk=/export/servers/jdk1.7.0_71 --with-debug-level=slowdebug --disable-zip-debug-info,执行配置依赖检查,生成Makefile,各参数含义如下:
其他选项和构建相关环境变量配置参考sudo sh ./configure --help的说明。检查完成会提示缺少的依赖包,大部分情况按照提示通过yum命令安装即可,如下图:
笔者反复执行上述安装命令一直报错,最后Google发现configue执行过程会记录日志config.log,查看具体的报错信息发现并不是缺少Xrender.h而是在加载XTest.h过程中发现XInput.h不存在,如下图:
进一步Google该头文件属于 libXi-devel包,执行sudo yum install libXi-devel后再configue,检查通过,如下图:
4、sudo make all执行编译,这个过程快慢依赖机器的硬件了,笔者大概花了10分钟左右, 下面是二次编译完成的截图:
5、编译完成,会在根目录下生成build文件夹,保存构建的结果和相关脚本,进入保存java启动入口二进制文件的jdk/bin目录下,执行./java -version,正常输出则编译成功,如下图:
参考:linux编译openjdk8
JAVA虚拟机学习笔记(一)Windows10下编译OpenJDK8
make 和 make install 的区别
因为C/C++本身不是严格的跨平台语言,所以最好保证开发环境跟编译环境一致,推荐使用Ubuntu桌面版Linux + Eclipse。不要使用Netbeans,不要使用Netbeans,不要使用Netbeans!!!刚开始笔者参考部分博客的引导使用Netbeans,Netbeans 8以上的新版本不支持C/C++,装上Netbeans 8 C/C++版查看OpenJDK源码就发现一堆莫名其妙的引用报错,找不到头文件,无法解析某个方法或者变量等,让人非常头大,最后放弃选择同样轻量级的IDE Eclipse。Eclipse C/C++开发通Java完全不同,也是不停磕磕碰碰。
那么常见的不带桌面系统的CentOS可以安装Eclipse么?笔者尝试过,不行,因为Eclipse的UI界面强依赖操作系统本身的桌面系统支持,CentOS可以安装GNOME或者KDE等桌面系统,或者安装X Window System,借助其他已安装桌面系统的CentOS来显示界面也可以,总之就是得伤筋动骨。
因为公司办公环境通常都是Windows,有没有办法在Windows上模拟Linux环境了?最简单粗暴的,在Window上借助Oracle VM VirtualBox安装一个虚拟机,在虚拟机内安装Ubuntu,前提是办公电脑配置高。还有一种比较省事的,远程开发,即代码开发在Windows上,IDE 通过sftp等方式将变动的代码同步远程Linux虚机上,利用远程Linux虚机的gcc/g++编译器编译调试代码,前提是能够直连这台Linux虚机并且有足够的权限,如今堡垒机盛行,这条路基本堵死了。最通用的解决办法是Cygwin或者MinGW,两者都是利用Windows本身的库函数来实现Linux上通用的GNU工具集合,如gcc编译器。MinGW是一种傻瓜式的按最低的标准配置安装,一键安装完成,安装包较小,但是缺乏灵活性,无法便捷安装其他依赖包。Cygwin在使用上相当于yum命令,提供上千种依赖供选择,并自动下载安装相关的依赖包,非常灵活,下面详细说明安装步骤:
1、到Cygwin官网(https://cygwin.com/install.html)下载最新的安装包,setup-x86_64.exe
2、点击执行,按照默认选项一直点下一步直到下图:
一开始会尝试从官网下载依赖,自然尝试失败,这儿是让我们输入国内可用的镜像地址,输入阿里云的镜像地址 http://mirrors.aliyun.com/cygwin/,注意空格,点击Add-->下一步,安装包检验镜像地址是否可用然后拉取必要的目录信息,然后进入下下图:
这是可供选择的各种依赖的列表,鼠标放到Full上可以查看各种View的含义:
这里我们直接输入gcc,g++,make,gdb,其中gcc是C的编译器,g++是C++的编译器,make是从源代码构建可执行文件的工具,gdb是调试工具,然后search即可, 如下图:
图中的带debuginfo后缀的依赖都是各个依赖本身编译时产生的调试信息,也可不安装。接着点下一步,如下图:
这是即将安装的依赖列表,第一次安装时,Cygwin会安装很多基础的依赖包如cd命令,ls命令等,第二次安装就只有你选择的依赖及其依赖的其他组件。继续点击下一步, 就会自动下载并安装了:
安装结束,点击桌面的Cygwin图标会弹出一个命令行界面,在此界面可以执行常用的Linux命令,如果提示不存在则可以重新执行上述步骤安装对应的依赖即可,如unzip。输入gcc --version,如果正确显示则安装成功,如下图:
Cygwin中默认字体较小,可右键options-->Text改变字体,配置快捷键等,如下图:
Cygwin在安装目录下会初始化类似于Linux的目录结构,启动后我们进入的目录就是C:\cygwin64\home\Administrator下,取决于当前用户的账户了,如下图:
至此Cygwin安装成功,下一步配置Eclipse,首先从官网下载支持C/C++版的Eclipse,如果是普通版则需要在Help-->Eclipse Marketplace中搜索cdt并安装了,如下图:
支持C/C++版的Eclipse默认安装该插件。
接着配置环境变量,将Cygwin的安装目录放到PATH中,方便Eclipse从PATH中自动识别gcc编译器,如下图:
配置完成就可以新建C/C++工程了,注意选择工程类型为C++ Managered Build,其他工程类型识别不了已安装的Cygwin,如下图:
点击Next,逐一选择ToolChains为Cygwin GCC,然后finish即可,如下图:
正常的项目会自动将Cygwin下include等包含头文件和依赖库的目录带出来,如果安装了第三方依赖库提示找不到对应头文件,则需要更改图中的include的配置了,如下图:
按照Java的做法,这时右键,Run as-->Local C/C++ Application,报错:
这个错误是因为Eclipse运行的时候没有在工程目录下找到二进制的可执行文件,很容易误以为是缺乏库文件一类的,解决办法是先执行Build,右键 Build Project即可,可在控制台看到构建执行的命令,在工程目录下看到生成的二级制可执行文件,如下图:
这时再执行右键,Run As就会成功了,如下图:
实际代码开发中经常需要测试某个函数,有没有类似Java JUnit的快捷的方法了?答案是没有,有单元测试框架但是没有跟IDE很好集成。有一种替代方案,利用命令行编译并执行单个C/C++文件。首先把工程建立在Cygwin的用户目录下,方便快速找到测试文件,注意不使用默认路径时需要加上项目的根目录,如下图:
接着创建src目录,创建新的source file文件,注意必须把文件后缀带上,因为存在.c,.h,.cpp,.hpp等多种后缀,IDE无法像创建Java文件一样自动加上.java后缀,如果不加则识别成普通的文本文件了,这时会给提示,如下图:
文件创建完成后,就可以在Cygwin的控制台中找到对应的文件了, 执行命令g++ -g CppTest.cpp -o CppTest生成二进制文件,注意生成的二进制文件是exe后缀的,因为windows操作系统只支持这种二进制文件,然后命令行运行该文件即可,如下图:
至此,整个开发环境就搭建好了,下一步就是导入OpenJDK的源码了,导入方式跟Java工程一样,New-->Import-->Existing Code As Makefile Project,输入源代码路径,如下图:
导入完成Eclipse会自动解析源代码中的各种引用和调用关系,自动配置必要的环境变量,然后就可以愉快的看代码了,如下图:
Eclipse比Netbeans强大的是不仅能解析正常的方法调用,也能准确找到同一个类在不同操作系统下的实现,可以帮助我们从整体上了解JVM的实现,如下图:
参考: windows 安装cygwin教程
Linux在远程X Server上显示图形界面
安装CentOS桌面环境
深入理解debuginfo
C++单元测试入门指南-在eclipse上建立Google test
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具,相比IDE中的调试工具,命令行的GDB更加强大,灵活,是Linux上调试内核驱动及C/C++应用程序的利器,GDB主要可帮助工程师完成下面4个方面的功能:
GDB支持的命令行选项通过gdb --help查看,输入gdb即启动gdb,默认会打印当前GDB的版本信息等,可以使用-q选项禁止输出,然后输入help选项,可以查看gdb提供各系列的调试命令,如breakpoints表示断点相关的命令,输入help breakpoints可以查看该系列下所有的命令,输入help break可以查看该命令的使用说明,如下图所示:
注意使用gdb命令调试前,需要保证gcc或者g++编译时加了-g选项,该选项用于生成调试信息,方便gdb获取当前执行的代码,行号,变量名等信息。下面演示下OpenJDK的调试步骤:
1、OpenJDK编译完成会在根目录下生成一个build目录,进入该目录即是编译的结果,如下图:
2、java的启动入口,即名为java的二进制文件在jdk/bin目录下,进入该目录,执行gdb -q ./java,表示调试java这个二进制文件,注意此时程序并未运行。如下图:
2、执行set args -version设置启动参数,执行set env _JAVA_LAUNCHER_DEBUG= 1设置环境变量,_JAVA_LAUNCHER_DEBUG为1表示会打印启动过程中的关键参数。
3、在不知道启动入口文件的前提下可以执行start命令,gdb会自动停留在启动入口main函数,然后可以查看该函数的文件信息,如果已经知道启动入口并设置了断点,则可以执行run命令,gdb会停到设置的第一个断点处。注意此时需要执行n命令表示进入到main.c这个文件的调试中,否则后面设置断点会无法准确定位到main.c这个文件,如下图:
4、找到启动入口了,接着在IDE中查看具体的代码,顺着代码的调用路径找到自己想要断点调试的地方,如下图:
导入工程后这个地方一直报错 Symbol 'FULL_VERSION' could not be resolved,是我们下载的代码有问题么?肯定不是代码问题,那这两个宏是哪来的了?答案在main.c引入的头文件defines.h中,如下图:
IDE的预处理器都走到#error这一步,说明当前整个工程代码都没有定义FULL_VERSION,JDK_MAJOR_VERSION,JDK_MINOR_VERSION这三个宏,但是我们编译成功,说明编译时肯定是有这三个宏的,那这三个宏的定义究竟在哪?全局搜索FULL_VERSION,结果只有这个defines.h有,如下图:
在源代码上已经找不到答案了,那就调试呗,看看实际穿进去的是啥值,通过b 125命令在这行代码处设置断点。
5、 执行c命令,即继续运行程序直到下一个断点处,此时我们还是看不到调用JLI_Launch具体的参数,可以执行s命令,表示调试这个函数的执行,此时调用参数就都打出来了,如下图:
从上述取值分析,这两个宏并不是源代码中写死的,而是在编译前的预处理环节由构建脚本根据构建配置动态生成的,具体的脚本位置还在探索中。
6、继续查看JLI_Launch函数的实现,我们想看InitLauncher函数的实现,然后Eclipse告诉我们有两种实现,分别是solaris和windows的,为啥没有Linux的呢?进一步查看与solaris同级的Linux目录下只有一个doc文件夹,难道是Linux的实现跟某个平台一样?如下图:
为了确定InitLauncher函数的实现,我们得debug该函数,gdb会告诉我们答案,执行n表示进入JLI_Launch所处的代码文件调试,执行b 207 打断点,执行c继续执行至该断点处,执行s进入该函数,答案是solaris下的试下,如下图:
为啥Linux可以使用跟solaris一样的实现了?答案是两者都是Unix的衍生版本,底层的库函数基本一致。
7、我们进入了InitLauncher函数的调试了,如果不想跳出该函数的调试怎么办?执行finish即可,然后gdb会进入到InitLauncher的下一个函数DumpState处,如下图:
8、至此gdb调试常用的命令介绍完了,执行c继续运行,居然报错了,如下图:
Program received signal SIGSEGV, Segmentation fault 表示可能应用程序访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址,难道这是gdb的bug??
9、观察报错输出的下一行Switching to Thread,切换到进程257947了,这行输出是啥意思?查看源码,JLI_Launch--》JVMInit--》ContinueInNewThread--》ContinueInNewThread0,参考注释,主要逻辑是创建一个新的线程,在新线程中创建JVM并调用main方法,具体执行的逻辑就是JavaMain了,如下图:
10、从源码得知,我们碰到多线程调试问题了,gdb是否支持多线程调试了?答案是肯定的,不过默认情况下只调试主进程,子进程会阻塞在创建进程的地方,可以执行info threads查看当前的进程信息,如下图:
标*号的进程为gdb当前调试中的进程,主进程是1,目前阻塞在pthread_join这个方法上,这个方法表示主进程休眠,直到子进程执行完任务并退出。
11、既然gdb已经切换到子进程了,那能不能继续调试子进程了?执行n,报错Cannot find bounds of current function,然后执行c,出现了正常java -version的输出:
从上图可知,实际创建了多个子进程,其中的一个子进程执行了版本信息打印,其他进程干啥了?啥时候产生的了?查看源码答案在JavaMain--》InitializeJVM--》CreateJavaVM中,参考注释,这是一个JNI调用了,不好直接调试了
打印java版本信息的方法在下面的PrintJavaVersion中,如下图:
printVersion这个属性在解析命令行参数的时候设置的,逻辑在JLI_Launch--》ParseArguments方法中,如下图:
PrintJavaVersion的实现也是调用这个类的sun/misc/Version的println方法实现的,同样是借助JNI,如下图:
之所以通过java类是实现而不是借助C代码,主要是利用java代码跨平台的特点,避免为不同的平台各自提供实现。
12、JNI调用的实现比较复杂,这里不做探讨,考虑下这个 sun/misc/Version类在哪呢?在项目根目录下执行find ./ -name Version.java命令,结果如下:
按照包名去匹配,总共有四个符合条件的,分别是gensrc,gen_profile_1,gen_profile_2,gen_profile_3下的,这四个都是在build目录下,也就是构建时生成的,通过less命令查看源代码,代码内容也是基本一致的,初步判断gen_profile_1,gen_profile_2,gen_profile_3这个应该是跟构建过程有关的,最终使用gensrc下的,而且在jdk的src下应该有生成Version.java的模板之类的,查找源码,jdk-->src-->share-->classes-->sun-->misc-->Version.java.template,答案如下:
13、OpenJDK的构建过程这里不做讨论,回到gdb调试,有没有办法调试PrintJavaVersion这段逻辑呢?执行b java.c:PrintJavaVersion 命令设置断点,然后执行run,中间一样报错Program received signal SIGSEGV,执行c继续往下运行,然后gdb就会停在PrintJavaVersion这了,如下图:
其他gdb命令的使用可以参考官方文档和如下链接:
Linux环境下段错误的产生原因及调试方法小结
Linux gdb调试器用法全面解析
使用gdb调试程序完全教程
GDB 调试多进程或者多线程应用
修改,编译,GDB调试openjdk8源码(docker环境下)
1、corba
CORBA 全称是Common Object Request Broker Architecture,CORBA 1.1 由对象管理组织在 1991 年发布,他定义了接口定义语言(IDL)和应用编程接口(API),从而通过实现对象请求代理(ORB)来激活客户/服务器的交互。CORBA 2.0 于 1994 年的 12 月发布,他定义了如何跨越不同的 ORB 提供者而进行通讯。在功能上基本等同于Webservice,即:
corba目录下为Java对该交互协议的实现,主要是Java代码。
参考:使用Java/CORBA实现分布应用编程
WebService学习总结(一)——WebService的相关概念
2、jaxp
JAXP的全称是Java API for XML Processing,是Java定义的一组操作XML文档的API,注意JAXP只是定义了API,并没有提供具体的实现,Java为了保证JAXP能够即装即用,默认带了Apache Xerces 解析器,代码位于jaxp/src/com/sun/org/apache下。 JAXP支持三种XML解析模型:
参考:JAXP 全面介绍,第 1 部分
Java xml解析器SAX解析与StAX解析比较
学习笔记——JAXP
3、jaxws
JAX-WS全称是JavaTM API forXML-Based Web Services, 是一组基于SOAP协议实现的Web Service的API,与之相对的是JAX-RS,全称是JavaTM API for RESTful Webservices,是一组基于RESTful风格的Http服务实现Web Service的API。JAX-WS相关代码位于jaxws/src/share/jaxws_classes,其中com目录下主要是JDK内部实现使用的类,对外暴露的API都在javax目录下,org目录是JDK内部依赖的relaxng包,relaxng同W3C XML Schema ,DTD一样都是定义XML文档规范的一种语言。
JAF全称是JavaBeans Activation Framework, JAF的目的在于统一处理不同数据格式的方法(不管数据格式为简单文本还是由图片、声音、视频甚至其它"活动"内容共同组成的复合文档),使得Java对象与编码数据流之间的映射变得非常容易,可以参考JavaMail中JAF的应用。JAF的相关代码位于jaxws/src/share/jaf_classes下。
参考:【webservice】Java JAX-WS和JAX-RS webservice
真正的轻量级WebService框架——使用JAX-WS(JWS)发布WebService
JAF框架及在JavaMail中的应用
4、langtools
langtools包下都是Java语言的支持工具实现,具体如下:
5、nashorn
JDK8 内嵌的Nashorn JavaScript 引擎,扩展了Java在JVM上运行动态JavaScript脚本的能力,但是因为ECMAScript规范发展较快,Nashorn相关代码很难维护,JDK11预计将移除相关代码。
参考: JDK 8 新特性 | Nashorn 脚本引擎
6、jdk
其中aix,bsd,linux,macosx,solaris,windows是各平台的特定实现,其中aix,bsd,linux,solaris都是类Unix系统,基本共用solaris的实现。share目录下是平台无关的实现,具体如下:
参考: java agent基础原理
JVMTI 和 Agent 实现
JDWP 协议及实现
7、hotspot
hotspot是JVM即JRE Java运行时环境的实现,具体如下:
├─agent Serviceability Agent实现,提供HSDB JVM调试工具
├─make 编译构建相关代码
├─src HotSpot VM的源代码
│ ├─cpu 不同CPU架构下CPU相关代码(汇编器、模板解释器、ad文件、部分runtime函数在这里实现)
│ ├─os 不同OS下系统调用相关代码
│ ├─os_cpu 跟OS和CPU架构相关的系统调用相关的代码,如用户栈操作
│ └─share 平台无关的共用代码
│ ├─tools 工具
│ │ ├─hsdis 反汇编插件,用于查看JIT即时编译器产生的汇编代码
│ │ ├─IdealGraphVisualizer 将server编译器的中间代码可视化的工具
│ │ ├─LogCompilation 将-XX:+LogCompilation输出的日志(hotspot.log)整理成更容易阅读的格式的工具
│ │ └─ProjectCreator 生成Visual Studio的project文件的工具
│ └─vm HotSpot VM的核心代码
│ ├─adlc 平台描述文件(上面的cpu或os_cpu里的*.ad文件)的编译器
│ ├─asm 汇编器接口
│ ├─c1 client编译器(又称“C1”)
│ ├─ci 动态编译器
│ ├─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 单元测试