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 不是我们自己编译的,看不到当中调用关系。例如以下图所看到的:


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




  • 相关阅读:
    CF1416D Graph and Queries
    Wordpress建站系统相关
    微观经济学
    Preface
    Thread pool in chromium
    [fllutter engine] 并发消息队列
    bugku misc
    python 3.1学习
    HTML&CSS
    DOM技术点
  • 原文地址:https://www.cnblogs.com/bhlsheji/p/4017816.html
Copyright © 2011-2022 走看看