zoukankan      html  css  js  c++  java
  • Java虚拟机的启动与程序的执行

    这篇文章是从 OpenJDK 源码的角度讲当我们执行了

    java -classpath . hello
    

    之后,java.exe 怎样从 main 函数開始运行,启动虚拟机,并运行字节码中的代码。

    实验环境

    要了解一个系统是怎样执行的,光看是不行的,要实际地执行,调试,改动才干对系统的动作方式有所了解。

    起初我是依照 GitHub 上的一个项目 OpenJDK-Research 在 windows 7 64位平台上,使用 Visual Studio 2010 来调试,执行的。可是后来发现,这个项目只编译了HotSpot虚拟机, java.exe 并没有编译。

    这里我们首先弄明确 java.exe 和虚拟机之间的关系。我们使用 Visual Studio 编译出的 HotSpot 是虚拟机,是作为动态链接库的形式被 java.exe 载入的。java.exe 负责解析參数,载入虚拟机链接库,它须要调用虚拟机中的函数来完毕运行 Java 程序的功能。所以,你在HotSpot的源码中找不到启动的程序的 main 函数,本来在 openjdk7 中,虚拟机是带有一个启动器的,在文件夹 openjdk/hotspot/src/share/tools/launcher/java.c 中能够找到 main 函数,可是在 openjdk8 中,这个启动器不见了,被放在 openjdk/jdk 文件夹下,而不是 openjdk/hotspot 文件夹下了,给我们的学习过程造成了伤害。

    所以我后来就在 linux 平台上调试了,由于在 windows 平台上,我始终没有把整个 openjdk8 编译成功,编译不出java.exe, 只编译了 hotspot,是看不到从 main 函数開始的运行的。关于怎样在 linux 平台下编译调试 openjdk8,能够參考我的还有一篇文章 在Ubuntu 12.04 上编译 openjdk8.

    调用栈

    jdk8u/jdk/src/share/bin/main.c::WinMain/main
      jdk8u/jdk/src/share/bin/java.c::JLI_Launch
        jdk8u/jdk/src/solaris/bin/java_md_solinux.c::LoadJavaVM # Load JVM Library: libjvm.so
        jdk8u/jdk/src/solaris/bin/java_md_solinux.c::JVMInit # Create JVM
          jdk8u/jdk/src/share/bin/java.c::ContinueInNewThread
            jdk8u/jdk/src/solaris/bin/java_md_solinux.c::ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
              pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args)
                jdk8u/jdk/src/share/bin/java.c::JavaMain
                  jdk8u/jdk/src/share/bin/java.c::InitializeJVM
                    jdk8uhotspotsrcsharevmprimsjni.cpp::JNI_CreateJavaVM
    
    
    

    运行过程

    • main.c (jdk8u/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:
    ");
                for (i = 0 ; i < __argc ; i++) {
                    printf("wwwd_args[%d] = %s
    ", 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 ,运行了不同的代码段,终于调用JLI_Launch 函数。

    • JLI_Lanuch(jdk8u/jdk/src/share/bin/java.c)
    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 */
    )
    {
    
    ...
    
        if (!LoadJavaVM(jvmpath, &ifn)) {
            return(6);
        }
    
    ...
    
        return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
    
    }
    

    从这里能够看出 JLI_Lanuch 的各个參数的含义, 我列出了关键代码, 当中 LoadJavaVM 完毕加载虚拟机动态链接库,并初始化 ifn 中的函数指针,HotSpot虚拟机就是这样向启动器 java 提供功能。

    • LoadJavaVM (jdk8u/jdk/src/solaris/bin/java_md_solinux.c)

    这个函数涉及动态链接库,不同操作系统有不同接口,这里是针对 linux 的。

    jboolean
    LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
    {
        ...
    
        libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
    
        ...
    
        ifn->CreateJavaVM = (CreateJavaVM_t)
        dlsym(libjvm, "JNI_CreateJavaVM");
    
        ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
        dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
    
        ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
            dlsym(libjvm, "JNI_GetCreatedJavaVMs");
    
      ...
    
    

    从这里能够看出加载动态链接库以及初始化 ifn 数据结构的代码。在我的调试版本号中,javapath 指向之前编译出的动态链接库 jdk8u/build/fastdebug/jdk/lib/i386/server/libjvm.so.

    • JVM_Init(jdk8u/jdk/src/solaris/bin/java_md_solinux.c)

    回到 JLI_Lanuch 函数,我们终于进入 JVM_Init, 这个函数会启动一个新线程。

    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 会调用还有一个函数 ContinueInNewThread0 启动线程,运行 JavaMain 函数:

    int
    ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
    
    ...
    
        if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
          void * tmp;
          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);
        }
    
    ...
    
    
    
    • JavaMain(jdk8u/jdk/src/share/bin/java.c)

    这个函数会初始化虚拟机,载入各种类,并运行应用程序中的 main 函数。凝视非常具体。

    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, end;
    
        RegisterThread();
    
        /* Initialize the virtual machine */
        start = CounterGet();
        if (!InitializeJVM(&vm, &env, &ifn)) {
            JLI_ReportErrorMessage(JVM_ERROR1);
            exit(1);
        }
    
        ...
    
        ret = 1;
    
        /*
         * Get the application's main class.
         *
         * See bugid 5030265.  The Main-Class name has already been parsed
         * from the manifest, but not parsed properly for UTF-8 support.
         * Hence the code here ignores the value previously extracted and
         * uses the pre-existing code to reextract the value.  This is
         * possibly an end of release cycle expedient.  However, it has
         * also been discovered that passing some character sets through
         * the environment has "strange" behavior on some variants of
         * Windows.  Hence, maybe the manifest parsing code local to the
         * launcher should never be enhanced.
         *
         * Hence, future work should either:
         *     1)   Correct the local parsing code and verify that the
         *          Main-Class attribute gets properly passed through
         *          all environments,
         *     2)   Remove the vestages of maintaining main_class through
         *          the environment (and remove these comments).
         *
         * This method also correctly handles launching existing JavaFX
         * applications that may or may not have a Main-Class manifest entry.
         */
        mainClass = LoadMainClass(env, mode, what);
        CHECK_EXCEPTION_NULL_LEAVE(mainClass);
        /*
         * In some cases when launching an application that needs a helper, e.g., a
         * JavaFX application with no main method, the mainClass will not be the
         * applications own main class but rather a helper class. To keep things
         * consistent in the UI we need to track and report the application main class.
         */
        appClass = GetApplicationClass(env);
        NULL_CHECK_RETURN_VALUE(appClass, -1);
        /*
         * PostJVMInit uses the class name as the application name for GUI purposes,
         * for example, on OSX this sets the application name in the menu bar for
         * both SWT and JavaFX. So we'll pass the actual application class here
         * instead of mainClass as that may be a launcher or helper class instead
         * of the application class.
         */
        PostJVMInit(env, appClass, vm);
        /*
         * 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);
    
        /* 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();
    }
    
    

    注意 InitializeJVM 函数,它会调用之前初始化的 ifn 数据结构中的 CreateJavaVM 函数.

    • InitializeJVM(jdk8u/jdk/src/share/bin/java.c::InitializeJVM)
    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:
        ");
            printf("version 0x%08lx, ", (long)args.version);
            printf("ignoreUnrecognized is %s, ",
                   args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
            printf("nOptions is %ld
    ", (long)args.nOptions);
            for (i = 0; i < numOptions; i++)
                printf("    option[%2d] = '%s'
    ",
                       i, args.options[i].optionString);
        }
    
        r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
        JLI_MemFree(options);
        return r == JNI_OK;
    }
    

    ifn->CreateJavaVM指向虚拟机动态链接库中的 JNI_CreateJavaVM 函数,这个函数会真正创建虚拟机。 这个函数运行后,pvm, penv 的值就会被设定,我们能够比較下运行前后它们的值,来看看它们的作用。

    // before r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    
    (gdb) p *pvm
    $8 = (JavaVM *) 0x0
    (gdb) p *penv
    $9 = (JNIEnv *) 0x0
    
    
    // after r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    
    (gdb) p ***penv
    $14 = {reserved0 = 0x0, reserved1 = 0x0, reserved2 = 0x0, reserved3 = 0x0, 
      GetVersion = 0xb6ede599 <jni_GetVersion>, 
      DefineClass = 0xb6eb20a0 <jni_DefineClass>, 
      FindClass = 0xb6eb253c <jni_FindClass>, 
      FromReflectedMethod = 0xb6eb2b17 <jni_FromReflectedMethod>, 
      FromReflectedField = 0xb6eb2edb <jni_FromReflectedField>, 
      ...
      ...
      }
    
    (gdb) p ***pvm
    $15 = {reserved0 = 0x0, reserved1 = 0x0, reserved2 = 0x0, 
      DestroyJavaVM = 0xb6edf1e8 <jni_DestroyJavaVM>, 
      AttachCurrentThread = 0xb6edf69a <jni_AttachCurrentThread>, 
      DetachCurrentThread = 0xb6edf795 <jni_DetachCurrentThread>, 
      GetEnv = 0xb6edf8d3 <jni_GetEnv>, 
      AttachCurrentThreadAsDaemon = 0xb6edfa7d <jni_AttachCurrentThreadAsDaemon>}
    

    能够看出它们得到了hotspot 中以 jni_ 开头的一些函数,虚拟机正是以这种方式向外提供功能。我们大概看一下JNI_CreateJavaVM 的功能。


    • JNI_CreateJavaVM(jdk8uhotspotsrcsharevmprimsjni.cpp)
    _JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
      ...
    
        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();
    
        // Tracks the time application was running before GC
        RuntimeService::record_application_start();
    
        // Notify JVMTI
        if (JvmtiExport::should_post_thread_life()) {
           JvmtiExport::post_thread_start(thread);
        }
    
        ...
    
      }
    
      ...
    
    }
    

    当中的 create_vm 函数是虚拟机初始化的关键,它初始化了虚拟机的大部分组件。另外能够看到 vm, penv 的值被设定。

    这个函数位于 jdk8uhotspotsrcsharevmprimsjni.cpp

    我之前在 Windows 下调试,直接调试的 HotSpot 动态链接库,能够看到的第一个函数就是 JNI_CreateJavaVM, 之前的调用都位于 java.exe 代码中。由于 Windows 中 java.exe 不是我们自己编译的,看不到当中调用关系。例如以下图所看到的:


    同一时候能够看到两个线程




  • 相关阅读:
    PAT 1097. Deduplication on a Linked List (链表)
    PAT 1096. Consecutive Factors
    PAT 1095. Cars on Campus
    PAT 1094. The Largest Generation (层级遍历)
    PAT 1093. Count PAT's
    PAT 1092. To Buy or Not to Buy
    PAT 1091. Acute Stroke (bfs)
    CSS:word-wrap/overflow/transition
    node-webkit中的requirejs报错问题:path must be a string error in Require.js
    script加载之defer和async
  • 原文地址:https://www.cnblogs.com/bhlsheji/p/4017816.html
Copyright © 2011-2022 走看看