OpenJDK8 Java程序启动解析(上) - Java程序是怎样执行到main方法的 置顶!
上篇主要简单分析一下JavaLauncher的启动过程,以及怎样执行到我们的Java main方法的. 其中涉及一些JNI的调用和类的解析加载. 这些部分本篇暂时不详细展开. 下篇会简单介绍下JNI部分虚拟机初始化.
这是跟我一起读OpenJDK源码的第二个系列.第一个系列为: OpenJDK8 编译构建基础设施详解,你可以在我的 跟我一起读OpenJDK源码 找到这些文章.
- OpenJDK8 编译构建基础设施详解(1) - A New OpenJDK Build-Infra Detail With GNU MAKE And AutoConf
- OpenJDK8 编译构建基础设施详解(2) - Make流程解析
- OpenJDK8 编译构建基础设施详解(3) - java命令是怎么编译出来的
现在我们已经知道了Java程序启动的大体框架. 我们的程序启动是从一个Launcher
入口开始执行的.而且这个Launcher入口被不同的Launcher共用. 在JDK的bin目录的各种命令都使用这个Launcher入口.具体为: ./jdk/src/share/bin/main.c
,关于Launcher
的更多细节参考上一篇:Java命令是怎么编译出来的,下面我们就结合源代码具体看一看整个过程是怎样到达Java类的Main函数的.
概要
我们先上一张整体流程的图.让大家有一个整体的大概认识.后面再结合着图与源代码一步步的解析.
结合着上图的标注, 启动过程会涉及到两个线程.一个是main函数执行所在的线程,线程名称为: Thread-1
(注: Linux平台),另外一个是JavaMain
方法所在的线程:Thread-2
, 大概的核心流程是在Thread-2
中完成. Thread-1
的main方法完成对JavaMain
的引导后,会处于阻塞等待状态. 直到Thread-2
执行完成.
虚拟机的创建与初始化,是使用JNI方法进行的. 在单独的JNI模块里面, 他是一个动态链接库. 在Linux平台下的名称是: libjvm.so
,它的目录大概是: /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
. 这个可以认为是JVM的本地实现的核心模块, 在此先有一个基本的概念就可以了.
注:
Launcher代码主要位置: https://github.com/AdoptOpenJDK/openjdk-jdk8u/tree/master/jdk/src/share/bin
这里没有给官方的hg的仓库,主要是为了在线阅读方便.
main
代码位置:
./jdk/src/share/main.c
代码链接: https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/main.c
代码作用: 作为对外的编译入口, 封装不同平台的入口函数差异. 直接调用JLI_Launch
函数
此处封装了windows平台和类unix平台不同的实现. windows平台是使用WinMain. 而Linux平台使用的C语言普通的main
函数. 出现这个不同的原因是: 在windows平台带GUI入口的WIN程序和控制台类型的程序入口是不一样的,而Java程序本身是要处理GUI相关的编程的.因此在windows平台.即使一般的java程序没有处理gui.其执行入口也是带GUI的WIN程序.
int WINAPI
WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)
{
int margc;
char** margv;
const jboolean const_javaw = JNI_TRUE;
__initenv
= _environ;
// ..........................
}
这里给出Linux平台的启动时的截图, 可以发现除了入口传入的margc
和margv
以外, 都是一些静态常量.我们可以看到有如下静态常量:
- const_jargs: null
- const_appclasspath: null
- FULL_VERSION
- DOT_VERSION
- const_progname : java
- const_launcher:
openjdk
- const_cpwildcard: 1
- const_javaw: 0
- const_ergo_class: 0
以上的大部分常量是在define.h
中定义的source链接:
#ifdef JAVA_ARGS
static const char* const_progname = "java";
static const char* const_jargs[] = JAVA_ARGS;
/*
* ApplicationHome is prepended to each of these entries; the resulting
* strings are concatenated (separated by PATH_SEPARATOR) and used as the
* value of -cp option to the launcher.
*/
#ifndef APP_CLASSPATH
#define APP_CLASSPATH { "/lib/tools.jar", "/classes" }
#endif /* APP_CLASSPATH */
static const char* const_appclasspath[] = APP_CLASSPATH;
#else /* !JAVA_ARGS */
#ifdef PROGNAME
static const char* const_progname = PROGNAME;
#else
static char* const_progname = NULL;
#endif
static const char** const_jargs = NULL;
static const char** const_appclasspath = NULL;
#endif /* JAVA_ARGS */
而我们知道,我们的编译时. 传入的宏定义或者参数如下:
注:
可以使用关键字在build.log中查找:Compiling *main.c* (for java)
以下的日志是从上篇文章中摘出来的.
/usr/bin/gcc\
-Wall\
-Wno-parentheses\
-Wextra\
-Wno-unused\
-Wno-unused-parameter\
-Wformat=2\
-pipe\
-fstack-protector\
-D_GNU_SOURCE\
-D_REENTRANT\
-D_LARGEFILE64_SOURCE\
-fno-omit-frame-pointer\
-D_LP64=1\
-D_LITTLE_ENDIAN\
-DLINUX\
-DARCH='"amd64"'\
-Damd64\
-DDEBUG\
-DRELEASE='"1.8.0-internal-debug"'\
-I/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/include\
-I/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/include/linux\
-I/sourcecode/jdk8u/jdk/src/share/javavm/export\
-I/sourcecode/jdk8u/jdk/src/solaris/javavm/export\
-I/sourcecode/jdk8u/jdk/src/share/native/common\
-I/sourcecode/jdk8u/jdk/src/solaris/native/common\
-fno-strict-aliasing\
-g\
-fPIE\
-I/sourcecode/jdk8u/jdk/src/share/bin\
-I/sourcecode/jdk8u/jdk/src/solaris/bin\
-I/sourcecode/jdk8u/jdk/src/linux/bin\
-DFULL_VERSION='"1.8.0-internal-debug-firfor_2022_02_15_20_53-b00"'\
-DJDK_MAJOR_VERSION='"1"'\
-DJDK_MINOR_VERSION='"8"'\
-DLIBARCHNAME='"amd64"'\
-DLAUNCHER_NAME='"openjdk"'\
-DPROGNAME='"java"'\
-DEXPAND_CLASSPATH_WILDCARDS\
-g\
-O0\
-DTHIS_FILE='"main.c"'\
-c\
-MMD\
-MF\
/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/objs/java_objs/main.d\
-o\
/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/objs/java_objs/main.o\
/sourcecode/jdk8u/jdk/src/share/bin/main.c
我们没有定义:JAVA_ARGS
, 所以: const_jargs
和const_appclasspath
都为NULL.由于已经在宏定义中定义的:PROGNAME
且为java
,因此const_progname
的值为java
,同理: const_launcher
也为传入的openjdk
. 其它参数就不一一分析了.
#ifdef JAVA_ARGS
...
#else
#ifdef PROGNAME
static const char* const_progname = PROGNAME;
#else
static char* const_progname = NULL;
#endif
static const char** const_jargs = NULL;
static const char** const_appclasspath = NULL;
#endif
另外这两个常量:pname
和lanme
似乎并没有特殊的作用. 只是传入后作为全局变量存储起来了而已.
# 位置: java.c
static const char *_program_name;
static const char *_launcher_name;
int
JLI_Launch(int argc, char ** argv, /* main argc, argc */
int jargc, const char** jargv, /* java args */
int appclassc, const char** appclassv, /* app classpath */
const char* fullversion, /* full version defined */
const char* dotversion, /* dot version defined */
const char* pname, /* program name */
const char* lname, /* launcher name */
jboolean javaargs, /* JAVA_ARGS */
jboolean cpwildcard, /* classpath wildcard*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy */
)
{
// ...... some code
_fVersion = fullversion; // 保存一些全局变量
_dVersion = dotversion;
_launcher_name = lname;
_program_name = pname;
_is_java_args = javaargs;
_wc_enabled = cpwildcard;
_ergo_policy = ergo;
在main.c中的方法基本什么都没做. 就是封装出编译的入口.好处理一些编译时确认的常量.然后调用JLI_Lanch
函数.
JLI_Lanch
文件位置:
./jdk/src/share/java.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java.c
代码作用: 加载hotspot虚拟机核心组件:libjvm.so
首先是调用:LoadJavaVM
加载libjvm.so
; 加载完成后数据存储到: InvocationFunctions;
int
JLI_Launch(int argc, char ** argv, /* main argc, argc */
int jargc, const char** jargv, /* java args */
int appclassc, const char** appclassv, /* app classpath */
const char* fullversion, /* full version defined */
const char* dotversion, /* dot version defined */
const char* pname, /* program name */
const char* lname, /* launcher name */
jboolean javaargs, /* JAVA_ARGS */
jboolean cpwildcard, /* classpath wildcard*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy */
)
{
InvocationFunctions ifn;
ifn.CreateJavaVM = 0;
ifn.GetDefaultJavaVMInitArgs = 0;
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
// ....... some code ......
}
我们看一下:InvocationFunctions 的定义.
// 代码定义: jdk/src/share/bin/java.h
/*
* Pointers to the needed JNI invocation API, initialized by LoadJavaVM.
*/
typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);
typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args);
typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
typedef struct {
CreateJavaVM_t CreateJavaVM;
GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
GetCreatedJavaVMs_t GetCreatedJavaVMs;
} InvocationFunctions;
里面存储了三个后面在初始化虚拟机时会用到的JNI函数指针.
LoadJavaVM
这个主要是根据平台加载动态链接库. linux环境的源文件信息为:
文件位置:jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 加载hotspot虚拟机核心组件:libjvm.so
, 返回三个函数指针: CreateJavaVM_t, GetDefaultJavaVMInitArgs_t, GetCreatedJavaVMs_t
jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
void *libjvm;
JLI_TraceLauncher("JVM path is %s\n", jvmpath);
libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
if (libjvm == NULL) {
#if defined(__solaris__) && defined(__sparc) && !defined(_LP64) /* i.e. 32-bit sparc */
// 省略此部分代码
#endif
JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
ifn->CreateJavaVM = (CreateJavaVM_t)
dlsym(libjvm, "JNI_CreateJavaVM");
if (ifn->CreateJavaVM == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
if (ifn->GetDefaultJavaVMInitArgs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs");
if (ifn->GetCreatedJavaVMs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
return JNI_TRUE;
}
JVMInit
文件位置:
jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 显示闪屏. 调用平台相关实现创建子线程.JavaMain
有点让人惊讶的是,这个文件的目录是在:solaris
. 这个我猜测是Sun公司最开始的时候.自己是有自己的Unix系统平台的. 而Unix平台与Linux平台又很像. (因为他们都遵循POSIX标准,关于这个标准.我想你有兴趣读一下:posix是什么都不知道,就别说你懂Linux了!)
代码本身没有几行. 看名称,第一行为显示java商标类似的功能. 没有深究. 后面就是一个创建新线程的调用. 这里的调用把之前得到的IFN 指针集合对象进行了传入.
int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
ShowSplashScreen();
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
ContinueInNewThread
文件位置:
jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 收集创建线程的必要参数JavaMainArgs并封装. 调用内部函数进行创建线程.并且传入了:JavaMain
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
/*
* If user doesn't specify stack size, check if VM has a preference.
* Note that HotSpot no longer supports JNI_VERSION_1_1 but it will
* return its default stack size through the init args structure.
*/
if (threadStackSize == 0) {
struct JDK1_1InitArgs args1_1;
memset((void*)&args1_1, 0, sizeof(args1_1));
args1_1.version = JNI_VERSION_1_1;
ifn->GetDefaultJavaVMInitArgs(&args1_1); /* ignore return value */
if (args1_1.javaStackSize > 0) {
threadStackSize = args1_1.javaStackSize;
}
}
/* ****************************************************************************** */
/* 收集参数,准备创建JVM的主线程, 同时调用Main方法 */
{ /* Create a new thread to create JVM and invoke main method */
JavaMainArgs args;
int rslt;
args.argc = argc;
args.argv = argv;
args.mode = mode;
args.what = what;
args.ifn = *ifn;
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
/* If the caller has deemed there is an error we
* simply return that, otherwise we return the value of
* the callee
*/
return (ret != 0) ? ret : rslt;
}
}
ContinueInNewThread0
文件位置:
jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 创建JavaMain线程, 并阻塞当前线程.
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
int rslt;
#ifndef __solaris__
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size);
}
/* *******************************************************************************************
* 创建线程: JavaMain
*/
if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
void * tmp;
// 创建完成后.当前线程在此等待
// Make calling thread wait for termination of the thread TH
pthread_join(tid, &tmp);
rslt = (int)tmp;
} else {
/*
* Continue execution in current thread if for some reason (e.g. out of
* memory/LWP) a new thread can't be created. This will likely fail
* later in continuation as JNI_CreateJavaVM needs to create quite a
* few new threads, anyway, just give it a try..
*/
rslt = continuation(args);
}
pthread_attr_destroy(&attr);
#else /* __solaris__ */
// solary implement. ommit it.....
#endif /* !__solaris__ */
return rslt;
}
JavaMain
文件位置:
jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 创建JavaMain线程, 并阻塞当前线程.
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;
jclass appClass = NULL; // actual application class being launched
jmethodID mainID;
jobjectArray mainArgs;
int ret = 0;
jlong start = 0, end = 0;
RegisterThread();
/* Initialize the virtual machine */
start = CounterGet();
/* ************************************************************************************************
* 虚拟机初始化调用. 通过调用libjvm.so的JNI方法完成初始化
*/
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
if (showSettings != NULL) {
ShowSettings(env, showSettings);
CHECK_EXCEPTION_LEAVE(1);
}
if (printVersion || showVersion) {
PrintJavaVersion(env, showVersion);
CHECK_EXCEPTION_LEAVE(0);
if (printVersion) {
LEAVE();
}
}
/* 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
*/
// 加载主类.
mainClass = LoadMainClass(env, mode, what);
CHECK_EXCEPTION_NULL_LEAVE(mainClass);
appClass = GetApplicationClass(env);
NULL_CHECK_RETURN_VALUE(appClass, -1);
PostJVMInit(env, appClass, vm);
CHECK_EXCEPTION_LEAVE(1);
/*
* 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 platform specific argument array */
mainArgs = CreateApplicationArgs(env, argv, argc);
CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
// 调用Main方法
/* 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();
}
以上是JavaMain的执行流程. 主要的核心一点是: 通过调用: InitializeJVM
进行JVM初始化. 这个实际调用的是: libjvm.so中的导出方法:JNI_CreateJavaVM,这个才是JVM初始化的核心. 这里我们先放一放他.后面专门抽一篇文章来分析他.
// 定义: jni.h
_JNI_IMPORT_OR_EXPORT_ jint JNICALL
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);
整体调试
为了能够比较直观地看到Launcher的运行.以及怎么到达JavaMain的. 可以在运行调试时添加环境变量:_JAVA_LAUNCHER_DEBUG=1
这样在运行的时候就会比较详细的打印出Launcher的启动日志.
/home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java -cp .:/home/firfor/sourcecode/train com.train.test.HelloWorld -Da=b -Dtestc=d
----_JAVA_LAUNCHER_DEBUG----
Launcher state:
debug:on
javargs:off
program name:java
launcher name:openjdk
javaw:off
fullversion:1.8.0-internal-debug-firfor_2022_02_15_20_53-b00
dotversion:1.8
ergo_policy:DEFAULT_ERGONOMICS_POLICY
Command line args:
argv[0] = /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java
argv[1] = -cp
argv[2] = .:/home/firfor/sourcecode/train
argv[3] = com.train.test.HelloWorld
argv[4] = -Da=b
argv[5] = -Dtestc=d
JRE path is /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk
jvm.cfg[0] = ->-server<-
jvm.cfg[1] = ->-client<-
44 micro seconds to parse jvm.cfg
Default VM: server
Does `/home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so' exist ... yes.
mustsetenv: FALSE
JVM path is /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
1639 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 6
option[ 0] = '-Dsun.java.launcher.diag=true'
option[ 1] = '-Djava.class.path=/home/firfor/ide/clion-2021.1.3/lib/bootstrap.jar:/home/firfor/ide/clion-2021.1.3/lib/util.jar:/home/firfor/ide/clion-2021.1.3/lib/jdom.jar:/home/firfor/ide/clion-2021.1.3/lib/log4j.jar:/home/firfor/ide/clion-2021.1.3/lib/jna.jar'
option[ 2] = '-Djava.class.path=.:/home/firfor/sourcecode/train'
option[ 3] = '-Dsun.java.command=com.train.test.HelloWorld -Da=b -Dtestc=d'
option[ 4] = '-Dsun.java.launcher=SUN_STANDARD'
option[ 5] = '-Dsun.java.launcher.pid=16523'
323422 micro seconds to InitializeJVM
Main class is 'com.train.test.HelloWorld'
App's argc is 2
argv[ 0] = '-Da=b'
argv[ 1] = '-Dtestc=d'
63183 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
hello, this file is compile by debug jdk
Process finished with exit code 0