性能文章>认识HotSpot虚拟机第4篇-HotSpot的启动过程>

认识HotSpot虚拟机第4篇-HotSpot的启动过程原创

https://a.perfma.net/img/2382850
1年前
632306

本文将详细介绍HotSpot的启动过程,启动过程涉及到的逻辑比较复杂,细节也比较多,下面我们开始以文章的形式简单介绍一下启动过程。

HotSpot通常会通过java.exe或javaw.exe来调用/jdk/src/share/bin/main.c文件中的main()函数来启动虚拟机,使用Eclipse进行调试时,也会调用到这个入口。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

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

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);
}

这个方法是Windows、UNIX、Linux以及Mac OS操作系统中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);
}

第一个参数,int型的argc,为整型,用来统计程序运行时发送给main函数的命令行参数的个数;第二个参数,char型的argv[],为字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。

2、JLI_Launch()函数

JLI_Launch()函数进行了一系列必要的操作,如libjvm.so的加载、参数解析、Classpath的获取和设置、系统属性的设置、JVM 初始化等。函数会调用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的定义如下:

源代码位置:/home/mazhi/workspace/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()函数后就可以看到有如下实现:

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()方法会在InitializeJVM()函数中调用,用来初始化2个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){
     ... 
    { /* 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()函数时,传递了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);
    }
 
    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()函数。

方法的第一个参数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; // actual application class being launched
    jmethodID mainID;
    jobjectArray mainArgs;
 
    // InitializeJVM 初始化JVM,给JavaVM和JNIEnv对象正确赋值,通过调用InvocationFunctions结构体下
    // 的CreateJavaVM()函数指针来实现,该指针在LoadJavaVM()函数中指向libjvm.so动态链接库中JNI_CreateJavaVM()函数
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    // ...  
 
    mainClass = LoadMainClass(env, mode, what);
 
    appClass = GetApplicationClass(env);
 
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");
 
    /* Build platform specific argument array */
    mainArgs = CreateApplicationArgs(env, argv, argc);
 
    /* Invoke main method. */
    (*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执行销毁。
搭建过程中如果有问题可直接评论留言或加作者微信mazhimazh。

作者持续维护的个人博客 http://www.classloading.com

点赞收藏
鸠摩

著有《深入解析Java编译器:源码剖析与实例详解》、《深入剖析Java虚拟机:源码剖析与实例详解》等书籍。公众号“深入剖析Java虚拟机HotSpot”作者

请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

记一次“雪花算法”造成的生产事故的排查记录

记一次“雪花算法”造成的生产事故的排查记录

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

单服务并发出票实践

单服务并发出票实践

刺激,线程池的一个BUG直接把CPU干到100%了。

刺激,线程池的一个BUG直接把CPU干到100%了。

6
0