专注虚拟机与编译器研究

第1.4篇-HotSpot VM的启动过程(配视频进行源码分析)

本文将详细介绍HotSpot的启动过程,启动过程涉及到的逻辑比较复杂,细节也比较多,为了让大家更快的了解这部分知识,我录制了对应的视频放到了B站上,大家可以参考。 

第4节-HotSpot的启动过程 

下面我们开始以文章的形式简单介绍一下启动过程。

HotSpot通常会通过JAVA_HOME目录下的bin/java或bin/javaw来调用/jdk/src/share/bin/main.c文件中的main()函数来启动虚拟机,使用Eclipse等IDEA进行调试时,也会调用到这个入口。main.c的main()函数负责创建运行环境,以及启动一个全新的线程去执行JVM的初始化和调用Java程序的main()方法。main()函数最终会阻塞当前线程,同时用另外一个线程去调用JavaMain()函数。main()函数的调用栈如下: 

main()                     main.c
JLI_Launch()               java.c
JVMInit()                  java_md_solinux.c
ContinueInNewThread()      java.c
ContinueInNewThread0()     java_md_solinux.c
pthread_join()             pthread_join.c

调用链的顺序从上到下,下面简单介绍一下涉及到的相关方法。

执行main()函数的线程最终会调用pthread_join()函数,这个函数会创建一个新的线程,而当前的线程阻塞在pthread_join()函数上,直到新线程执行结束。

1、main()函数

main()函数的实现如下: 

源代码位置:/openjdk/jdk/src/share/bin/main.c

#ifdef JAVAW

char **__initenv;

int WINAPI WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow){
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_TRUE;

    __initenv = _environ;

#else /* JAVAW */
int main(int argc, char **argv){
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_FALSE;
#endif /* JAVAW */
#ifdef _WIN32
    {
        int i = 0;
        if (getenv(JLDEBUG_ENV_ENTRY) != NULL) {
            printf("Windows original main args:\n");
            for (i = 0 ; i < __argc ; i++) {
                printf("wwwd_args[%d] = %s\n", i, __argv[i]);
            }
        }
    }
    JLI_CmdToArgs(GetCommandLine());
    margc = JLI_GetStdArgc();
    // add one more to mark the end
    margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
    {
        int i = 0;
        StdArg *stdargs = JLI_GetStdArgs();
        for (i = 0 ; i < margc ; i++) {
            margv[i] = stdargs[i].arg;
        }
        margv[i] = NULL;
    }
#else /* *NIXES */
    margc = argc;
    margv = argv;
#endif /* WIN32 */
    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);
}

main()函数是Windows、UNIX、Linux以及Mac 操作系统中C/C++的入口函数,而Windows的入口函数和其它的不太一样。为了尽可能重用代码,这里使用#ifdef条件编译。对于基于Linux内核的Ubuntu操作系统来说,最终编译执行的代码如下:

int main(int argc, char **argv){
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_FALSE;
    margc = argc;
    margv = 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);
}

main()函数的第一个参数argc是int类型,用来统计程序运行时发送给main函数的命令行参数的个数;第二个参数argv是char**类型,可以看作字符串数组,用来存放指向字符串参数的指针数组,数组中的每一个元素指向一个参数。

2、JLI_Launch()函数

JLI_Launch()函数进行了一系列必要的操作,如libjvm.so的加载、参数解析、Classpath的获取和设置、系统属性的设置、JVM 初始化等。libjvm.so就是具体的虚拟机实现,只不过被编译为了动态链接库而已。函数会调用LoadJavaVM()加载libjvm.so并初始化相关参数,调用语句如下:

LoadJavaVM(jvmpath, &ifn)

其中jvmpath就是"/home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so",也就是libjvm.so的存储路径,而ifn是InvocationFunctions类型变量,InvocationFunctions的定义如下: 

源代码位置:openjdk/jdk/src/share/bin/java.h
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;

可以看到结构体InvocationFunctions中定义了3个函数指针,3个函数的实现在libjvm.so这个动态链接库中,查看LoadJavaVM()函数后就可以看到有如下实现:

// dlsym()函数可以获取动态链接库中变量或函数的地址,这里获取的是3个函数的地址
ifn->CreateJavaVM = (CreateJavaVM_t) dlsym(libjvm, "JNI_CreateJavaVM");
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t) dlsym(libjvm, "JNI_GetCreatedJavaVMs");

所以通过函数指针调用时,最终会调用到libjvm.so中对应的以JNI_Xxx开头的函数,也就是如上代码的JNI_CreateJavaVM等函数。其中,JNI_CreateJavaVM()函数会调用InitializeJVM()函数,用来初始化JNI调用时非常重要的2个参数JavaVM和JNIEnv,后面在介绍JNI时会详细介绍,这里不做过多介绍。

3、JVMInit()函数

JVMInit()函数的源代码如下: 

源代码位置:openjdk/jdk/src/solaris/bin/java_md_solinux.c
int JVMInit(InvocationFunctions* ifn, jlong threadStackSize, int argc, char **argv, int mode, char *what, int ret){ // ... return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret); }

这个方法中没有特别的逻辑,直接调用continueInNewThread()函数继续执行相关逻辑。

4、ContinueInNewThread()函数

在JVMInit()函数中调用的ContinueInNewThread()函数的实现如下: 

源代码位置:openjdk/jdk/src/share/bin/java.c

int ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
                    int argc, char **argv,
                    int mode, char *what, int ret){
    // ... 
    { 
      JavaMainArgs args;
      int rslt;

      args.argc = argc;
      args.argv = argv;
      args.mode = mode;
      args.what = what;
      args.ifn = *ifn;
      
      // 调用如下函数创建一个HotSpot VM实例并执行Java应用中主类的main()方法
      rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
      return (ret != 0) ? ret : rslt;
    }
}

在调用ContinueInNewThread0()函数时,传递了JavaMain函数指针和调用此函数需要的参数args。

5、ContinueInNewthread0()函数

ContinueInNewThread()函数调用的ContinueInNewThread0()函数的实现如下: 

源代码位置:openjdk/jdk/src/solaris/bin/java_md_solinux.c

int ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
    int rslt;
    ...
    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);
    }

    // 调用pthread_create()函数创建一个线程,这个新线程将执行continuation函数指针指向的函数JavaMain
    if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
      void * tmp;
      // 当前线程会阻塞在这里,直到新创建的线程执行结束
      pthread_join(tid, &tmp);  
      rslt = (int)tmp;
    }

    pthread_attr_destroy(&attr);
    ...
    return rslt;
}

在Linux 系统中(后面所说的Linux系统都是指基于Linux内核的操作系统)创建一个 pthread_t 线程,然后使用这个新创建的线程执行JavaMain()函数。

ContinueInNewThread0()函数的第一个参数int (JNICALL continuation)(void )接收的就是JavaMain()函数的指针。关于指针函数与函数指针、以及Linux下创建线程的相关知识点后面会介绍,到时候这里会给出链接。

下面看一下JavaMain()函数的实现,如下: 

源代码位置:openjdk/jdk/src/share/bin/java.c

int JNICALL  JavaMain(void * _args){
    JavaMainArgs *args = (JavaMainArgs *)_args;
    int argc = args->argc;
    char **argv = args->argv;
    InvocationFunctions ifn = args->ifn;

    JavaVM *vm = 0;
    JNIEnv *env = 0;
    jclass mainClass = NULL;
    jclass appClass = NULL; 
    jmethodID mainID;
    jobjectArray mainArgs;

    // InitializeJVM()函数会调用InvocationFunctions结构体下
    // 的CreateJavaVM函数指针指向的函数来创建并初始化一个HotSpot VM实例,
   //  CreateJavaVM()函数在LoadJavaVM()函数中指向libjvm.so动态链接库中
    // JNI_CreateJavaVM()函数,此函数    // 定义在vm/prims/jni.cpp文件中
    // InitializeJVM()函数还会给JavaVM和JNIEnv对象赋值
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    // ...  
    // 加载Java应用程序的主类(也就是含有main()方法的类)
    mainClass = LoadMainClass(env, mode, what);
    
    appClass = GetApplicationClass(env);
    // 从Java主类中查找main()方法对应的唯一id
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");

    // 创建传递给Java主类main()方法的参数
    mainArgs = CreateApplicationArgs(env, argv, argc);

    // 调用Java主类的main()方法
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    // ...
}

以上代码主要找出Java应用程序的main()方法,然后调用并执行。

1、调用InitializeJVM()函数初始化JVM,主要就是初始化2个非常重要的变量JavaVM与JNIEnv,在这里不过多探讨这个问题,后面在讲解JNI调用时会详细介绍初始化过程;

2、调用LoadMainClass()函数获取Java程序的启动类,对于前面举过的实例来说,由于配置了参数 “com.test/Test", 所以会查找com.test.Test类。LoadMainClass()函数最终会调用libjvm.so中实现的JVM_FindClassFromBootLoader()方法来查找启动类,涉及到的逻辑比较多,后面在讲解类型的加载时会介绍;

3、调用GetStaticMethodId()函数查找Java启动方法,其实就是获取Test类中的main()方法;

4、调用JNIEnv中定义的CallStaticVoidMethod()方法,最终会调用JavaCalls::call()函数执行Test类中的main()方法。JavaCalls:call()函数是个非常重要的方法,后面在讲解方法执行引擎时会详细介绍。

以上步骤都还在当前线程的控制下。当控制权转移到Test.main()之后当前线程就不再做其它事儿了,等Test.main()函数返回之后,当前线程会清理和关闭JVM。调用本地函数jni_DetachCurrentThread()断开与主线程的连接。当成功与主线程断开连接后,当前线程一直等待程序中所有的非守护线程全部执行结束,然后调用本地函数jni_DestroyJavaVM()对JVM执行销毁。

公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到70+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流

 

posted on 2020-11-18 09:17  鸠摩(马智)  阅读(1673)  评论(2编辑  收藏  举报

导航