我们都知道当写java的main函数时,启动java进程,就能从main函数开始作为程序的入口开始执行。
public static void main(String[] args) {
}
那么启动java进程到底是怎么执行到public static void main(String[] args)的。
以下代码均以openJDK为例,分支是jdk8-b99
首先先看java进程的入口函数
/jdk/src/share/bind/main.c
int
main(int argc, char **argv)
{
// ...省略其他代码
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), const_jargs,
sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
FULL_VERSION,
DOT_VERSION,
(const_progname != NULL) ? const_progname : *margv,
(const_launcher != NULL) ? const_launcher : *margv,
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
const_cpwildcard, const_javaw, const_ergo_class);
}
/jdk/src/share/bind/java.c
// 省略代码
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
// 省略代码
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
LoadJavaVM是加载JVM初始化的一些前置处理,我们先忽略,先看主干。主干JVMInit是初始化JVM。
LoadJavaVM和JVMInit有三个实现,分别是:
- LINUX:/jdk/src/solaris/bin/java_md_solinux.c
- MACOS:/jdk/src/macosx/bin/java_md_macosx.c
- WINDOWS:/jdk/src/windows/bin/java_md.c
以下以LINUX的实现为例
/jdk/src/solaris/bin/java_md_solinux.c
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/share/bind/java.c
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
// 省略代码
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
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
// 省略代码
if (thr_create(NULL, stack_size, (void *(*)(void *))continuation, args, flags, &tid) == 0) {
void * tmp;
thr_join(tid, NULL, &tmp);
rslt = (int)tmp;
} else {
/* See above. Continue in current thread if thr_create() failed */
rslt = continuation(args);
}
#endif /* __linux__ */
return rslt;
}
可以看到最后执行了continuation这个函数。这个函数是ContinueInNewThread0函数的入参传进来的,因此再返回ContinueInNewThread函数,可以看到代码ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args),因此实际执行的是JavaMain的这个函数,我们继续看JavaMain这个函数。
/jdk/src/share/bind/java.c
int JNICALL
JavaMain(void * _args)
{
// 省略代码
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
// 省略代码
mainClass = LoadMainClass(env, mode, what);
// 省略代码
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);
/* 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
- 加载java main方法
- LoadMainClass:加载java main方法所在的主类到内存中
- GetStaticMethodID:获取jmethodID
- CallStaticVoidMethod:调用JNI执行java main方法
jmethodID根据是类+方法名+签名生成 签名是方法的入参和出参构成 例如:([Ljava/lang/String;)V表示入参为string数组,返回值为void
接下来我们看InitializeJVM做了什么事情
/jdk/src/share/bind/java.c
static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{
JavaVMInitArgs args;
jint r;
memset(&args, 0, sizeof(args));
args.version = JNI_VERSION_1_2;
args.nOptions = numOptions;
args.options = options;
args.ignoreUnrecognized = JNI_FALSE;
if (JLI_IsTraceLauncher()) {
int i = 0;
printf("JavaVM args:\n ");
printf("version 0x%08lx, ", (long)args.version);
printf("ignoreUnrecognized is %s, ",
args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
printf("nOptions is %ld\n", (long)args.nOptions);
for (i = 0; i < numOptions; i++)
printf(" option[%2d] = '%s'\n",
i, args.options[i].optionString);
}
r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
JLI_MemFree(options);
return r == JNI_OK;
}
可以看到关键代码行执行了一个指针ifn->CreateJavaVM,从函数名上看,这个是创建了jvm的逻辑。那这个指针是怎么来的,可以看到ifn是作为InitializeJVM的入参传进来的,因此我们返回JavaMain函数去看InitializeJVM函数的入参是怎么来的。再次贴出JavaMain的部分代码
/jdk/src/share/bind/java.c
可以看到ifn是从JavaMain的入参_args中取出来的,那我们再继续往上走
/jdk/src/solaris/bin/java_md_solinux.c
/jdk/src/share/bind/java.c
/jdk/src/solaris/bin/java_md_solinux.c
回到了我们一开始入口的JLI_Launch方法,由于代码太多,我省略了一些代码 /jdk/src/share/bind/java.c
InvocationFunctions ifn;
// 省略代码
ifn.CreateJavaVM = 0;
ifn.GetDefaultJavaVMInitArgs = 0;
// 省略代码
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
// 省略代码
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
可以看到这个ifn在这里定义了ifn.CreateJavaVM = 0,然后传入到了LoadJavaVM里面,不知道做了什么事情后,在JVMInit->ContinueInNewThread->ContinueInNewThread0->JavaMain中执行。那我们看看LoadJavaVM做了什么事情
/jdk/src/solaris/bin/java_md_solinux.c
可以看到,ifn->CreateJavaVM这个指针被赋值JNI的JNI_CreateJavaVM这个函数,也就是执行指针时,相当于执行了JNI_CreateJavaVM函数。至此,我们再重新梳理一下流程如下图:
LoadJavaVM中赋值了ifn->CreateJavaVM的值为JNI_CreateJavaVM这个函数。然后在InitializeJVM调用了这个指针,相当于又执行了JNI_CreateJavaVM这个函数。我们继续看一下这个函数的逻辑。jni相关的函数,一般存放在/hotspot/src/share/vm/prims/jni.cpp的文件下。
/hotspot/src/share/vm/prims/jni.cpp
可以看到核心方法是Threads::create_vm,再看一下Threads::create_vm做了什么。Threads::create_vm的逻辑比较复杂,我截取部分逻辑简单讲解一下
/hotspot/src/share/vm/runtime/thread.cpp
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
// ...
// 参数与属性解析,包括环境变量,启动参数等,下面是启动参数解析的代码
jint parse_result = Arguments::parse(args);
// ...
// 扩展加载
if (Arguments::init_agents_at_startup()) {
// 如果有java agent的启动参数,则初始化java agent
create_vm_init_agents();
}
// ...
// 初始化java的synchronization
ObjectMonitor::Initialize() ;
// ...
// 全局初始化,全局初始化执行很多事情,它的函数我们就不详细看
// 包括:初始化JVM管理模块(包括线程服务-ThreadService、运行时服务-RuntimeService和类加载统计-ClassLoadingService)、
// 加载并验证JVM字节码指令集、引导类加载器、代码缓存、虚拟机版本、初始化操作系统相关模块、存根
// Java内存模型相关 (堆、Metaspace、方法缓存、符号表、字符串表等)
// 解释器与编译器初始化、垃圾回收初始化、引用标记初始化等
jint status = init_globals();
}
至此我们知道了创建JVM的流程,接下去我们回过头继续看java main方法是如何执行的
如图,回顾一下原来的提到的CallStaticVoidMethod,它是实际执行java main方法的地方,我们看到它是env的函数,我们再次看一下JavaMain的简略代码
/jdk/src/share/bind/java.c
JavaMain(void * _args)
{
// 省略代码
JNIEnv *env = 0;
// 省略代码
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
// 省略代码
/* 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();
}
可以看到env是一个JNIEnv,然后在InitializeJVM里面赋值。在InitializeJVM的代码了,又是传递到 ifn->CreateJavaVM里面赋值,从上面得知,这个指针实际上调用的是JNI_CreateJavaVM函数,于是我们到JNI_CreateJavaVM中查看代码如下
result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
if (result == JNI_OK) {
JavaThread *thread = JavaThread::current();
/* thread is thread_in_vm here */
*vm = (JavaVM *)(&main_vm);
*(JNIEnv**)penv = thread->jni_environment();
我们再查看JNIEnv_(由于typedef JNIEnv_ JNIEnv,所以JNIEnv=JNIEnv_)这个结构体,发现它定义了很多函数。
/hotspot/src/share/vm/prims/jni.h
这些函数是调用了JNINativeInterface_的指针。我们再看JNINativeInterface_的指针是什么时候被赋值的
可以看到是JavaThread有个set_jni_functions的方法设置的,如下
然后再找谁调用了set_jni_functions,如下
猜测,JNI_CreateJavaVM里面的Threads::create_vm初始化了JavaThread,并调用了JavaThread::initialize,JavaThread::initialize调用了set_jni_functions(jni_functions())完成赋值。我们再看一下jni_functions()的代码逻辑
/hotspot/src/share/vm/prims/jni.cpp
可以看到,在这里定义了一个jni_NativeInterface变量,为JNINativeInterface_的指针赋值了各种jni的函数(函数表)。
/hotspot/src/share/vm/prims/jni.cpp
实际上执行的jni_CallStaticVoidMethod函数,我们再来看一下jni_CallStaticVoidMethod函数里面的逻辑
/hotspot/src/share/vm/prims/jni.cpp
可以看到这是一个宏定义,我们把它宏展开,大概是这样子的
extern "C" {
void JNICALL jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...) {
JavaThread* thread = JavaThread::thread_from_jni_environment(env);
assert(!VerifyJNIEnvThread || (thread == Thread::current()), "JNIEnv 仅在同一线程有效");
ThreadInVMfromNative __tiv(thread); // 将线程状态从 Native 切换到 VM 模式
debug_only(VMNativeEntryWrapper __vew;);
// TRACE 调用(根据编译条件是否启用)
HandleMarkCleaner __hm(thread); // 自动管理本地引用表
Thread* THREAD = thread;
os::verify_stack_alignment();
WeakPreserveExceptionMark __wem(thread);
JNIWrapper("CallStaticVoidMethod");
#ifndef USDT2
DTRACE_PROBE3(hotspot_jni, CallStaticVoidMethod__entry, env, cls, methodID);
#else /* USDT2 */
HOTSPOT_JNI_CALLSTATICVOIDMETHOD_ENTRY(
env, cls, (uintptr_t) methodID);
#endif /* USDT2 */
DT_VOID_RETURN_MARK(CallStaticVoidMethod);
va_list args;
va_start(args, methodID);
JavaValue jvalue(T_VOID);
JNI_ArgumentPusherVaArg ap(methodID, args);
jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
va_end(args);
}
}
这段代码的大概意思是插入了c语言的片段,c语言的片段定义了jni_CallStaticVoidMethod函数 jni_CallStaticVoidMethod是jni提供的用于操作java方法的jni函数。至于具体怎么操作的就涉及到了操作机器码完成入栈等操作,这里就不详细讲述了。 最后附上整体的调用图