java的main函数如何执行

172 阅读7分钟

我们都知道当写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。

image.png

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的代码比较多,我截取了比较核心的,概括起来就是如下图所示

image.png

  • 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

image.png 可以看到ifn是从JavaMain的入参_args中取出来的,那我们再继续往上走

/jdk/src/solaris/bin/java_md_solinux.c image.png

/jdk/src/share/bind/java.c image.png

/jdk/src/solaris/bin/java_md_solinux.c image.png

回到了我们一开始入口的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 image.png

可以看到,ifn->CreateJavaVM这个指针被赋值JNI的JNI_CreateJavaVM这个函数,也就是执行指针时,相当于执行了JNI_CreateJavaVM函数。至此,我们再重新梳理一下流程如下图:

image.png

LoadJavaVM中赋值了ifn->CreateJavaVM的值为JNI_CreateJavaVM这个函数。然后在InitializeJVM调用了这个指针,相当于又执行了JNI_CreateJavaVM这个函数。我们继续看一下这个函数的逻辑。jni相关的函数,一般存放在/hotspot/src/share/vm/prims/jni.cpp的文件下。

/hotspot/src/share/vm/prims/jni.cpp

image.png 可以看到核心方法是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方法是如何执行的

image.png

如图,回顾一下原来的提到的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 image.png 这些函数是调用了JNINativeInterface_的指针。我们再看JNINativeInterface_的指针是什么时候被赋值的 可以看到是JavaThread有个set_jni_functions的方法设置的,如下

image.png 然后再找谁调用了set_jni_functions,如下

image.png

猜测,JNI_CreateJavaVM里面的Threads::create_vm初始化了JavaThread,并调用了JavaThread::initialize,JavaThread::initialize调用了set_jni_functions(jni_functions())完成赋值。我们再看一下jni_functions()的代码逻辑

/hotspot/src/share/vm/prims/jni.cpp image.png

可以看到,在这里定义了一个jni_NativeInterface变量,为JNINativeInterface_的指针赋值了各种jni的函数(函数表)。 /hotspot/src/share/vm/prims/jni.cpp image.png 实际上执行的jni_CallStaticVoidMethod函数,我们再来看一下jni_CallStaticVoidMethod函数里面的逻辑

/hotspot/src/share/vm/prims/jni.cpp image.png 可以看到这是一个宏定义,我们把它宏展开,大概是这样子的

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函数。至于具体怎么操作的就涉及到了操作机器码完成入栈等操作,这里就不详细讲述了。 最后附上整体的调用图

image.png