目录
学习一个复杂项目源码的最关键的一步是找准应用启动和对外提供服务的入口,从这些入口处开始顺藤摸瓜式的查看代码,可以对项目的各功能模块有一个整体宏观上的认识,并以此为切入点,有的放矢,按需深入了解各功能模块的实现细节,这是最高效的学习源码的方式。JVM的启动入口在哪了?可以借助GDB的start命令查看。
1、JVM启动入口
OpenJDK的源码下载和编译可以参考Java程序员自我修养——OpenJDK8 编译,调试和目录结构,编译完成可以写一个HelloWorld.java作为测试代码,如下:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
将其保存在编译后生成/build/linux-x86_64-normal-server-slowdebug/jdk/bin/目录下,执行./javac HelloWorld.java将其编译成class文件,执行./java HelloWorld,程序正确打印则编译成功。执行gdb -q ./java进入gdb调试,执行set args HelloWorld设置启动参数,即执行./java的参数,执行start命令,gdb停在启动入口处,如下图:
启动入口在/home/openjdk/openjdk-jdk8u-master/jdk/src/share/bin/main.c:97,查看代码可知其核心启动方法是JLI_Launch。
2、JLI_Launch
其主要流程如下:
- SelectVersion,从jar包中manifest文件或者命令行读取用户使用的JDK版本,判断当前版本是否合适
- CreateExecutionEnvironment,设置执行环境参数
- LoadJavaVM,加载libjvm动态链接库,从中获取JNI_CreateJavaVM,JNI_GetDefaultJavaVMInitArgs和JNI_GetCreatedJavaVMs三个函数的实现,其中JNI_CreateJavaVM是JVM初始化的核心入口,具体实现在hotspot目录中
- ParseArguments 解析命令行参数,如-version,-help等参数在该方法中解析的
- SetJavaCommandLineProp 解析形如-Dsun.java.command=的命令行参数
- SetJavaLauncherPlatformProps 解析形如-Dsun.java.launcher.*的命令行参数
- JVMInit,通过JVMInit->ContinueInNewThread->ContinueInNewThread0->pthread_create创建了一个新的线程,执行JavaMain函数,主线程pthread_join该线程,在JavaMain函数中完成虚拟机的初始化和启动。
执行export _JAVA_LAUNCHER_DEBUG=1设置环境变量,然后可以查看整个启动过程的日志,如下图:
3、可移植性
如点击LoadJavaVM时,eclipse弹框如下:
该函数的实现到底是哪个文件了?为啥没有Linux的实现了?Linux和solaris都是类Unix系统,所以这里应该是共用solaris的实现,怎么证明了?用eclipse 全局搜索包含java_md_solinux.c文件名的文件,其中位于jdk目录下的只有一个文件,CoreLibraries.gmk,如下图:
如果不是macosx和windows则执行BUILD_LIBJLI_FILES += java_md_solinux.c ergo.c,即将 java_md_solinux.c这个文件加入到编译路径中,从而使用java_md_solinux.c的实现。除通过这种构建脚本的方式实现各操作系统兼容外,JVM还使用了宏定义和预处理的方式,如java_md.h中有一段代码,如下:
当定义了宏MACOSX以后,就包含java_md_macosx.h,否则包含java_md_solinux.h,这两个头文件包含了特定于该操作系统的特殊头文件和变量定义。类似这种代码在hotspot目录下特别常见,JVM通常将公共的代码放在share目录下,特定于操作系统的代码放在该操作系统的目录下,然后通过上述两种方式在编译打包时根据构建脚本配置或者宏定义编译打包成特定于操作系统的JDK,如下图:
这类宏是执行配置检查的configure根据所在的操作系统的特点在编译时自动注入进去,但是eclipse没有执行configure这个过程,需要给编译器添加宏定义,从而让预处理器能够正确的包含头文件。添加方法是,在C++的工程上点击右键,选择properties,然后如下图操作点击Add:
接着如下图,添加宏定义:
点击OK,然后Apply and Close后编译器自动重新执行代码的预处理,引入正确的头文件。
4、JavaMain
JLI_Launch作为启动器,创建了一个新线程执行JavaMain函数,JLI_Launch所在的线程称为启动线程,执行JavaMain函数的称之为Main线程。JavaMain函数的主要流程如下:
- InitializeJVM 初始化JVM,给JavaVM和JNIEnv对象正确赋值,通过调用InvocationFunctions结构体下的CreateJavaVM方法指针实现,该指针在LoadJavaVM方法中指向libjvm动态链接库中JNI_CreateJavaVM函数。
- LoadMainClass 获取应用