在开始正式深入学习HotSpot的源码之前,大家首先需要明白HotSpot的源码目录结构是怎样构成的,以及每一个目录中所包含的特定功能模块实现是什么。当只有在彻底弄清楚这些问题后,才能更有针对性的阅读和学习HotSpot的源码。尽管HotSpot只是OpenJDK项目中的一个子集,但是HotSpot却在整个OpenJDK项目中占据了非常重要的地位,所以本书只会针对HotSpot项目的源码进行讲解,大家只需关注HotSpot即可。
在解压后的HosSpot源码目录下,主要由agent、make、src和test这4个子目录构成了HosSpot源码的整体目录结构。其中agent目录下包含了Serviceability Agent的客户端实现。make目录下包含了用于build出HotSpot的各种配置文件。而src目录则是其中最重要的一个子目录,该目录下包含了HotSpot的所有源码实现,比如:与CPU的相关实现、与操作系统的相关实现、与平台无关的通用实现、HotSpot的核心功能实现、各类GC的实现以及执行引擎的相关实现等。至于test目录下仅仅只是包含了HotSpot相关的一些单元测试用例。
在此需要提醒大家,尽管Java继承了C语言的语法结构,并改编了C++语言的对象模型,但是彼此之间仍然存在较大的语法差异。所以Java开发人员在阅读HotSpot的源码时,千万不要在语法细节上钻牛角尖,只需关注具体的数据结构和算法即可。
HotSpot的源码目录结构,如下所示(目录整理自撒加大神的博文):
├─agent Serviceability Agent的客户端实现
├─make 用于build出HotSpot的各种配置文件
├─src HotSpot的所有源代码
│ ├─cpu CPU相关代码
│ ├─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 单元测试
当大家清楚HotSpot的源码目录结构后,才能更快的熟悉HotSpot每一个目录下所包含的特定功能模块实现。
2.2 Launcher简介
Launcher是一种用于启动JVM进程的启动器。在Java中,Launcher可以根据类别划分为2种。一种是正式版的启动器,也就是大家在Windows平台下经常使用到的java.exe和javaw.exe程序。前者在运行时会保留控制台,以及显示程序的输出信息。而后者主要是用于执行Java的GUI程序,也就是说,使用javaw.exe执行Java程序时将不会显示程序的输出信息。关于Launcher的具体用法和标准选项配置,大家可以在控制台中输入命令“java -help”,如下所示:
具体用法:
java [-options] class [args...](执行类)
或
java [-options] -jar jarfile [args...](执行jar文件)
其中选项包括:
-d32 | 使用32位数据模型(如果可用) |
-d64 | 使用64位数据模型(如果可用) |
-client | 选择"client"VM |
-server | 选择"server"VM |
-hotspot | 是"client"VM的同义词[已过时]默认VM是client |
-cp | 目录和zip/jar文件的类搜索路径 |
-classpath | 目录和zip/jar文件的类搜索路径用“;”分隔的目录,JAR档案和ZIP 档案列表, 用于搜索类文件 |
-D<name>=<value> | 设置系统属性 |
-verbose[:class|gc|jni] | 启用详细输出 |
-version | 输出产品版本并退出 |
-version:<value> | 需要指定的版本才能运行 |
-showversion | 输出产品版本并继续 |
-jre-restrict-search|-no-jre-restrict-search | 在版本搜索中包括/排除用户专用JRE |
-? -help | 输出此帮助消息 |
-X | 输出非标准选项的帮助 |
-ea[:<packagename>...|:<classname>] |
|
-enableassertions[:<packagename>...|:<classname>] | 按指定的粒度启用断言 |
-disableassertions[:<packagename>...|:<classname>] | 禁用具有指定粒度的断言 |
-esa | -enablesystemassertions | 启用系统断言 |
-dsa | -disablesystemassertions | 禁用系统断言 |
-agentlib:<libname>[=<options>] | 加载本机代理库<libname>,例如-agentlib:hprof另请参阅 -agentlib:jdwp=help和-agentlib:hprof=help |
-agentpath:<pathname>[=<options>] | 按完整路径名加载本机代理库 |
-javaagent:<jarpath>[=<options>] | 加载Java编程语言代理, 请参阅java.lang.instrument |
-splash:<imagepath> | 使用指定的图像显示启动屏幕 |
大家千万不要认为Launcher就是虚拟机实现,其实从严格意义上来说,Launcher只是一个封装了虚拟机的执行外壳,由它负责装载JRE环境和Windows平台下的jvm.dll动态链接库(Linux平台下则是装载libjvm.so)。在一个JVM的进程内部,只能执行一个指定的Java程序,也就是说,当执行多个Java程序时,也就意味着同时启动了多个JVM进程。在1.5.6小节中,本书示例了如何编译一个Debug版本的HotSpot,所以为了调试跟踪方面,大家可以使用Java的另一种启动器gamma。在HotSpot中Launcher是使用C语言编写的,对比gamma和java后不难发现两者的源码几乎是一模一样的,仅存在少量差异,也就是说,在OpenJDK中gamma和java是共用的同一套Launcher源码实现。gamma的源码在HotSpot的源码目录下,大家可以在/hotspot/src/share/tools/launcher/java.c中找到。而java却并非包含在HotSpot的源码目录下,而是包含在/jdk/src/share/bin/main.c中。
尽管Launcher并非是HotSpot的核心,甚至应该算是HotSpot中比较“外围”的功能模块,但既然是这样,笔者为什么还需要大费周章的对Launcher的源码进行剖析?其实了解Launcher的执行原理是非常有意义的,Launcher既然是JVM的启动器,那么必然会由它负责调用HotSpot的核心代码对JVM进行初始化,以及由它负责维护JVM的整个生命周期。所以理解了Launcher的执行原理,才是迈进HotSpot的第一步。
2.3 跟踪Launcher的执行过程
本书并非只是一本单纯讲解HotSpot原理的理论性读物,从本章开始,大家将会从以往的枯燥和乏味中深入到HotSpot的具体实现细节上。对于那些曾经想要深入研究JVM技术却又止步于源码面前的Java开发人员而言,本书的知识点将会是你们迫切想要得到的答案。
尽管每一个Java开发人员对Launcher的使用都非常熟悉,但这并不代表对Launcher的执行过程也了如指掌,所以本章将会重点讲解关于Launcher的执行过程并剖析源码细节。由于HotSpot的Launcher是采用C语言编写的,所以具备一定的C语言功底,必然是最好不过的。但如果你仅仅只是专攻于Java技术,也并非无法理解本章的一些源码示例,毕竟Java的语法结构就是继承自C语言,所以一些简单C语法相信你也一定能够理解。在正式开始讲解之前,笔者还是需要再次重申一次关于源码的阅读方式,希望大家千万不要过多停留在语法细节上,只需关注具体的数据结构和算法即可。
2.3.1 使用Launcher启动JVM
Launcher从启动到结束的整个执行链路,如图2-1所示。当成功启动Launcher后,会首先进入到Launcher的启动函数中,这一点和Java程序一样,Launcher的启动函数同样也是main()。main()函数的主要任务是负责创建运行环境以及启动一个全新的线程去执行JVM的初始化和调用Java程序的main()方法。
图2-1 Launcher的执行过程
当main()函数成功创建运行后,就会启动一个全新的线程去调用JavaMain()函数,而JavaMain()函数的主要任务是负责调用InitializeJVM()函数。顾名思义InitializeJVM()函数肯定会负责JVM初始化的相关工作,但InitializeJVM()函数本身却并不具备初始化JVM的能力,而是由它调用本地函数JNI_CreateJavaVM()去完成真正意义上的JVM初始化。
当JVM初始化完成后,Launcher接着调用LoadClass()函数和GetStaticMethodId()函数,分别获取Java程序的启动类和启动方法。当这2个步骤执行完后,Launcher就会调用本地函数jni_CallStaticVoidMethod()执行Java程序的main()方法。
最后Launcher还会调用本地函数jni_DetachCurrentThread()断开与主线程的连接,当成功与主线程断开连接后,Launcher就会一直等待程序中所有的非守护线程(non-daemon thread)全部执行结束,然后调用本地函数jni_DestroyJavaVM()对JVM执行销毁。在此需要提醒大家,在JDK1.2版本之前,只有主线程才允许对JVM执行销毁,而在JDK1.2及后续版本中则没有此限制,非主线程也允许对JVM执行销毁。
2.3.2 启动函数main()
当Launcher成功启动后,首先会进入到main()函数中对与运行环境相关的局部变量进行初始化。初始化后的局部变量不仅在程序后续创建运行环境时需要使用到,在调用JavaMain()函数时,也需要将这些变量传递过去,如下所示:
代码2-1 初始化与运行环境相关的局部变量
/* 初始化与运行环境相关的局部变量 */ |