前言
java的跨平台是如何实现的,为了提升性能又做了哪些工作,所以学习jdk源码很有必要。我们知道hotspot是C++写的,其实里面有大量的汇编直接操作CPU寄存器,C++的很多特性和具体的汇编指令我们不必要全部了解,大概知道有栈创建对象、堆创建对象,方法调用,宏定义等基本的就可以。通过学习hotspot源码会对加深计算机的操作系统、CPU寄存器、内存以及指针压缩、线程栈内存分配、大页内存、NUMA架构等了解,下面就让我们进入hotspot源码的世界。
源码下载
openjdk地址:http://openjdk.java.net
jdk(jdk7):http://hg.openjdk.java.net/jdk7/jdk7/jdk/archive/tip.zip
jvm(hotspot):http://hg.openjdk.java.net/jdk8/jdk8/hotspot/archive/tip.zip
说明一下,这里的jdk只是我们JAVA开发环境的JAVA API及实现部分,而hotspot是就是我们JAVA开发环境JDK里面的JRE其中JVM部分。
工具安装
1、eclipse c++版本 :
eclipse 官网下载页面搜索框输入c++即可下载最新版本
https://iso.mirrors.ustc.edu.cn/eclipse/technology/epp/downloads/release/2021-06/R/eclipse-cpp-2021-06-R-win32-x86_64.zip
2、安装java 插件
安装插件输入java,选择最简单的java program language
导入项目
导入源码后调整一下jdk里面代码路径,将src/share/classes子目录拷贝到src目录下。
这个就是我们熟悉的JRE中rt.jar的源码(重点关注java安装包的src源码中不包含的sun目录),调整代码后就不会出现代码路径问题。
熟悉目录结构
jdk以及hotspot的源码目录结构有相同点,就是基础相同的代码在share目录,操作系统及CPU有区别的分别放置各自目录,比如jdk中除了share目录有windows、solaris(solaris、linux等unix派系),这个就是操作系统相关的代码。Hotspot虚拟机的源码除了share目录还有os(操作系统相关)、cpu(cpu架构指令相关)、os_cpu(操作系统和CPU架构相关)目录。
源码阅读小技巧
hotspot源码是c、c++写的,阅读起来比较困难,经常有类 或 方法不知道哪里来的,eclipse也跟踪不进去,我们一般看include了哪些头文件,按住CTRL点不进去的代码先在include的头文件找,有的是多层include,实在找不着的就全局搜索了。比如jdk的share/bin/main.c中方法JLI_Launch不知道哪里来的,我们看导main.c里面有#include "defines.h", 就去defines.h里面找,发现defines.h里面有#include "java.h",那我们基本知道是java.h或java.c里面定义的JLI_Launch。
C C++小知识
new对象
栈上分配对象 ObjectClass obj;
堆上分配对象 ObjectClass *obj = new ObjectClass(); ,调用方法用->
方法调用
1、::类静态方法调用 Lable p = Label::create();
2、->类对象方法指针调用 p.create();
3、.类对象方法实例调用 (p).create();
引用与指针
&标识引用
标识指针 (p) 标识指针指向的值
指针和引用都可以做为参数,func(int *i) 、 func(int &i)
正式开始
我们都知道java程序运行过程,由main方法或者jar的manifest文件定义的启动类的main方法开始,这个过程又是怎样的呢,就从JVM启动开始一步一步探究。
java Test.class
则调jdk的src/share/bin/main.c中JLI_Launch方法
JLI_Launch我通过#include "defines.h"引入,defines.h中又引入了java.h
因此我们找到了java.c中定义的JLI_Launch方法
这个方法里面前面都是校验和准备工作,判断CPU架构、找到jre路径、解析java启动命令带的参数、装载动态库jvm.all或jvm.so(LoadJavaVM)等等,最后一步是ContinueInNewThread方法,启动了一个线程,也就是我们的main主线程。
ContinueInNewThread方法中调用ContinueInNewThread0方法,
ContinueInNewThread0方法我们只能在java.h中有定义,而java.h中有引入#include "java_md.h" ,这个根据操作系统不同实现不一样,我们看一下unix派系操作系统的实现,代码在jdk的src/solaris/bin/java_md.c。
意思是根据线程栈大小创建线程,然后从continuation地址开始,等待CPU调度,也就是CPU执行的是continuation地址对应的代码,我们再回过头看下java.c中的调用传入参数,rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args); 因此实际调用的是java.c中的JavaMain方法。
//java.c
int JNICALL
JavaMain(void * _args)
{
JavaMainArgs *args = (JavaMainArgs *)_args;
int argc = args->argc;
char **argv = args->argv;
int mode = args->mode;
char *what = args->what;
InvocationFunctions ifn = args->ifn;
JavaVM *vm = 0;
JNIEnv *env = 0;
jclass mainClass = NULL;
jmethodID mainID;
jobjectArray mainArgs;
int ret = 0;
jlong start, end;
/* Initialize the virtual machine */
start = CounterGet();
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
if (printVersion || showVersion) {
PrintJavaVersion(env, showVersion);
CHECK_EXCEPTION_LEAVE(0);
if (printVersion) {
LEAVE();
}
}
if (showSettings != NULL) {
ShowSettings(env, showSettings);
CHECK_EXCEPTION_LEAVE(1);
}
/* If the user specified neither a class name nor a JAR file */
if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
PrintUsage(env, printXUsage);
CHECK_EXCEPTION_LEAVE(1);
LEAVE();
}
FreeKnownVMs(); /* after last possible PrintUsage() */
if (JLI_IsTraceLauncher()) {
end = CounterGet();
JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
(long)(jint)Counter2Micros(end-start));
}
/* At this stage, argc/argv have the application's arguments */
if (JLI_IsTraceLauncher()){
int i;
printf("%s is '%s'\n", launchModeNames[mode], what);
printf("App's argc is %d\n", argc);
for (i=0; i < argc; i++) {
printf(" argv[%2d] = '%s'\n", i, argv[i]);
}
}
ret = 1;
/*
* Get the application's main class.
*
* See bugid 5030265. The Main-Class name has already been parsed
* from the manifest, but not parsed properly for UTF-8 support.
* Hence the code here ignores the value previously extracted and
* uses the pre-existing code to reextract the value. This is
* possibly an end of release cycle expedient. However, it has
* also been discovered that passing some character sets through
* the environment has "strange" behavior on some variants of
* Windows. Hence, maybe the manifest parsing code local to the
* launcher should never be enhanced.
*
* Hence, future work should either:
* 1) Correct the local parsing code and verify that the
* Main-Class attribute gets properly passed through
* all environments,
* 2) Remove the vestages of maintaining main_class through
* the environment (and remove these comments).
*/
mainClass = LoadMainClass(env, mode, what);
CHECK_EXCEPTION_NULL_LEAVE(mainClass);
/*
* The LoadMainClass not only loads the main class, it will also ensure
* that the main method's signature is correct, therefore further checking
* is not required. The main method is invoked here so that extraneous java
* stacks are not in the application stack trace.
*/
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
CHECK_EXCEPTION_NULL_LEAVE(mainID);
/* Build argument array */
mainArgs = NewPlatformStringArray(env, argv, argc);
CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
/* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
/*
* The launcher's exit code (in the absence of calls to
* System.exit) will be non-zero if main threw an exception.
*/
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
LEAVE();
}
准备阶段判断如果JVM没有初始化则初始化JVM(InitializeJVM),初始化JVM先创建JVM(CreateJavaVM),这个会通过JNI创建虚拟机,我们后面再看下JVM是如何创建的。
JVM准备好了再执行main方法,核心有4步,1:mainClass = LoadMainClass(env, mode, what); 加载主class文件; 2: mainID = (env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); 获取静态方法main方法;3:mainArgs = NewPlatformStringArray(env, argv, argc); 获取方法参数列表;4:(env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);调用main方法。
后面章节将会详细讲解这个主方法,解开我们的疑惑1:class是如何加载的,2:main方法是怎么调用。