• JVM系列(二):jvm是如何加载java代码的?


      上一篇粗略讲了下jvm的启动过程,但很多路子还没跑通。其中非常核心的,加载vm的过程。这个可以在hotspot中找到端倪。但jvm启动,又是如何载入java代码呢。

    1. JavaMain加载流程

      我们知道,java中入口是在main方法中,可以在命令行中指定main类,或者jar包中指定的manifest.xml中指定的main类。在java.c中,我们可以看到一个JavaMain方法,不知从何而来,但很像是直接加载java入口的方法。

    // share/bin/java.c
    // 加载 main 函数类
    // 通过引入 JavaMain(), 接入java方法
    // #define JNICALL __stdcall
    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;
        // 一些jvm的调用实例,在之前的步骤中,通过加载相应动态链接方法,保存起来的
        /** 
         * ifn->CreateJavaVM =
         *   (void *)GetProcAddress(handle, "JNI_CreateJavaVM");
         * ifn->GetDefaultJavaVMInitArgs =
         *   (void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs");
         */
        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;
        // collector
        RegisterThread();
        /* Initialize the virtual machine */
        start = CounterGet();
        // 初始化jvm,失败则退出
        if (!InitializeJVM(&vm, &env, &ifn)) {
            JLI_ReportErrorMessage(JVM_ERROR1);
            exit(1);
        }
        // jvm检查完毕,如果只是一些展示类请求,则展示信息后,退出jvm
        if (showSettings != NULL) {
            ShowSettings(env, showSettings);
            /**
             * 宏是神奇的操作,此处 *env 直接引用
    #define CHECK_EXCEPTION_LEAVE(CEL_return_value) 
        do { 
            if ((*env)->ExceptionOccurred(env)) { 
                JLI_ReportExceptionDescription(env); 
                ret = (CEL_return_value); 
                LEAVE(); 
            } 
        } while (JNI_FALSE)
             */
            CHECK_EXCEPTION_LEAVE(1);
        }
        // 调用 LEAVE() 方法的目的在于主动销毁jvm线程
        // 且退出当前方法调用,即 LEAVE() 后方法不再被执行
    /*
     * Always detach the main thread so that it appears to have ended when
     * the application's main method exits.  This will invoke the
     * uncaught exception handler machinery if main threw an
     * exception.  An uncaught exception handler cannot change the
     * launcher's return code except by calling System.exit.
     *
     * Wait for all non-daemon threads to end, then destroy the VM.
     * This will actually create a trivial new Java waiter thread
     * named "DestroyJavaVM", but this will be seen as a different
     * thread from the one that executed main, even though they are
     * the same C thread.  This allows mainThread.join() and
     * mainThread.isAlive() to work as expected.
     */
        /**
         *
         * 
    #define LEAVE() 
        do { 
            if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { 
                JLI_ReportErrorMessage(JVM_ERROR2); 
                ret = 1; 
            } 
            if (JNI_TRUE) { 
                (*vm)->DestroyJavaVM(vm); 
                return ret; 
            } 
        } while (JNI_FALSE)
         */
        if (printVersion || showVersion) {
            PrintJavaVersion(env, showVersion);
            CHECK_EXCEPTION_LEAVE(0);
            if (printVersion) {
                LEAVE();
            }
        }
        /* If the user specified neither a class name nor a JAR file */
        if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
            PrintUsage(env, printXUsage);
            CHECK_EXCEPTION_LEAVE(1);
            LEAVE();
        }
        // 释放内存
        FreeKnownVMs();  /* after last possible PrintUsage() */
        if (JLI_IsTraceLauncher()) {
            end = CounterGet();
            JLI_TraceLauncher("%ld micro seconds to InitializeJVM
    ",
                   (long)(jint)Counter2Micros(end-start));
        }
        /* At this stage, argc/argv have the application's arguments */
        if (JLI_IsTraceLauncher()){
            int i;
            printf("%s is '%s'
    ", launchModeNames[mode], what);
            printf("App's argc is %d
    ", argc);
            for (i=0; i < argc; i++) {
                printf("    argv[%2d] = '%s'
    ", i, argv[i]);
            }
        }
        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.
         */
        // 加载 main 指定的class类
        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.
         */
        // 加载main() 方法前执行初始化
        PostJVMInit(env, appClass, vm);
        CHECK_EXCEPTION_LEAVE(1);
        /*
         * 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.
         */
        // 获取main()方法id, main(String[] args)
        mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                           "([Ljava/lang/String;)V");
        CHECK_EXCEPTION_NULL_LEAVE(mainID);
        /* Build platform specific argument array */
        // 构建args[] 参数
        mainArgs = CreateApplicationArgs(env, argv, argc);
        CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
        /* Invoke main method. */
        // 调用java实现的main()方法
        // XX:: 重要实现
        (*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();
    }
    /*
     * Loads a class and verifies that the main class is present and it is ok to
     * call it for more details refer to the java implementation.
     */
    static jclass
    LoadMainClass(JNIEnv *env, int mode, char *name)
    {
        jmethodID mid;
        jstring str;
        jobject result;
        jlong start, end;
        jclass cls = GetLauncherHelperClass(env);
        NULL_CHECK0(cls);
        if (JLI_IsTraceLauncher()) {
            start = CounterGet();
        }
        // checkAndLoadMain(String) 方法作为中间main()调用
        NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                    "checkAndLoadMain",
                    "(ZILjava/lang/String;)Ljava/lang/Class;"));
        str = NewPlatformString(env, name);
        CHECK_JNI_RETURN_0(
            result = (*env)->CallStaticObjectMethod(
                env, cls, mid, USE_STDERR, mode, str));
        if (JLI_IsTraceLauncher()) {
            end   = CounterGet();
            printf("%ld micro seconds to load main class
    ",
                   (long)(jint)Counter2Micros(end-start));
            printf("----%s----
    ", JLDEBUG_ENV_ENTRY);
        }
        return (jclass)result;
    }    
    
    // 初始化jvm, 主要是调用 CreateJavaVM() 方法,进行创建jvm操作
    /*
     * Initializes the Java Virtual Machine. Also frees options array when
     * finished.
     */
    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;
    }

      略去核心加载jvm的实现,要加载main类,还是比较简单的。主要就是通过前面查找出的main_class, 然后通过在jvm的实例中获取到的各方法的函数指针,然后按照字节码的规范,调用 MainClass.main(String[]) 方法。当然了,为了兼容其他非main的场景,它还有很多附加处理逻辑。

           另外,如何翻译java代码,并执行,这是jvm的重中之重。即编译原理的拿手好戏,也是我们普通程序员越不过的坎!但至少,我们来到了门槛前面!

    2. jvm启动框架

      前面讲了加载main方法的过程,大致理解了c如何启动调用java的main的。那么,这又是如何调用准备的呢,在这之前都需要做哪些准备呢?

      实际上,这是平台相关的实现。

    // share/bin/java.c
    /*
     * Entry point.
     */
    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 */
    )
    {
        int mode = LM_UNKNOWN;
        char *what = NULL;
        char *cpath = 0;
        char *main_class = NULL;
        int ret;
        InvocationFunctions ifn;
        jlong start, end;
        char jvmpath[MAXPATHLEN];
        char jrepath[MAXPATHLEN];
        char jvmcfg[MAXPATHLEN];
        _fVersion = fullversion;
        _dVersion = dotversion;
        _launcher_name = lname;
        _program_name = pname;
        _is_java_args = javaargs;
        _wc_enabled = cpwildcard;
        _ergo_policy = ergo;
        // 初始化启动器
        InitLauncher(javaw);
        // 打印状态
        DumpState();
        // 跟踪调用启动
        if (JLI_IsTraceLauncher()) {
            int i;
            printf("Command line args:
    ");
            for (i = 0; i < argc ; i++) {
                printf("argv[%d] = %s
    ", i, argv[i]);
            }
            AddOption("-Dsun.java.launcher.diag=true", NULL);
        }
        /*
         * Make sure the specified version of the JRE is running.
         *
         * There are three things to note about the SelectVersion() routine:
         *  1) If the version running isn't correct, this routine doesn't
         *     return (either the correct version has been exec'd or an error
         *     was issued).
         *  2) Argc and Argv in this scope are *not* altered by this routine.
         *     It is the responsibility of subsequent code to ignore the
         *     arguments handled by this routine.
         *  3) As a side-effect, the variable "main_class" is guaranteed to
         *     be set (if it should ever be set).  This isn't exactly the
         *     poster child for structured programming, but it is a small
         *     price to pay for not processing a jar file operand twice.
         *     (Note: This side effect has been disabled.  See comment on
         *     bugid 5030265 below.)
         */
        // 解析命令行参数,选择一jre版本
        SelectVersion(argc, argv, &main_class);
        CreateExecutionEnvironment(&argc, &argv,
                                   jrepath, sizeof(jrepath),
                                   jvmpath, sizeof(jvmpath),
                                   jvmcfg,  sizeof(jvmcfg));
        if (!IsJavaArgs()) {
            // 设置一些特殊的环境变量
            SetJvmEnvironment(argc,argv);
        }
        ifn.CreateJavaVM = 0;
        ifn.GetDefaultJavaVMInitArgs = 0;
        if (JLI_IsTraceLauncher()) {
            start = CounterGet();
        }
        // 加载VM, 重中之重
        if (!LoadJavaVM(jvmpath, &ifn)) {
            return(6);
        }
        if (JLI_IsTraceLauncher()) {
            end   = CounterGet();
        }
        JLI_TraceLauncher("%ld micro seconds to LoadJavaVM
    ",
                 (long)(jint)Counter2Micros(end-start));
        ++argv;
        --argc;
        // 解析更多参数信息
        if (IsJavaArgs()) {
            /* Preprocess wrapper arguments */
            TranslateApplicationArgs(jargc, jargv, &argc, &argv);
            if (!AddApplicationOptions(appclassc, appclassv)) {
                return(1);
            }
        } else {
            /* Set default CLASSPATH */
            cpath = getenv("CLASSPATH");
            if (cpath == NULL) {
                cpath = ".";
            }
            SetClassPath(cpath);
        }
        /* Parse command line options; if the return value of
         * ParseArguments is false, the program should exit.
         */
        // 解析参数
        if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
        {
            return(ret);
        }
        /* Override class path if -jar flag was specified */
        if (mode == LM_JAR) {
            SetClassPath(what);     /* Override class path */
        }
        /* set the -Dsun.java.command pseudo property */
        SetJavaCommandLineProp(what, argc, argv);
        /* Set the -Dsun.java.launcher pseudo property */
        SetJavaLauncherProp();
        /* set the -Dsun.java.launcher.* platform properties */
        SetJavaLauncherPlatformProps();
        return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
    }
    
    // macos/bin/java_md_macos.c
    // MacOSX we may continue in the same thread
    int
    JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
                     int argc, char **argv,
                     int mode, char *what, int ret) {
        if (sameThread) {
            JLI_TraceLauncher("In same thread
    ");
            // need to block this thread against the main thread
            // so signals get caught correctly
            __block int rslt = 0;
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
            {
                NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock: ^{
                    JavaMainArgs args;
                    args.argc = argc;
                    args.argv = argv;
                    args.mode = mode;
                    args.what = what;
                    args.ifn  = *ifn;
                    // 调用 JavaMain()
                    rslt = JavaMain(&args);
                }];
                /*
                 * We cannot use dispatch_sync here, because it blocks the main dispatch queue.
                 * Using the main NSRunLoop allows the dispatch queue to run properly once
                 * SWT (or whatever toolkit this is needed for) kicks off it's own NSRunLoop
                 * and starts running.
                 */
                [op performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:YES];
            }
            [pool drain];
            return rslt;
        } else {
            return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
        }
    }

      以上是mac调用 javaMain的方式。在windows, 以及linux上则稍有不同。

    // windows/bin/java_md.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);
    }
    // java.c
    int
    ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
                        int argc, char **argv,
                        int mode, char *what, int ret)
    {
    
        /*
         * If user doesn't specify stack size, check if VM has a preference.
         * Note that HotSpot no longer supports JNI_VERSION_1_1 but it will
         * return its default stack size through the init args structure.
         */
        if (threadStackSize == 0) {
          struct JDK1_1InitArgs args1_1;
          memset((void*)&args1_1, 0, sizeof(args1_1));
          args1_1.version = JNI_VERSION_1_1;
          ifn->GetDefaultJavaVMInitArgs(&args1_1);  /* ignore return value */
          if (args1_1.javaStackSize > 0) {
             threadStackSize = args1_1.javaStackSize;
          }
        }
    
        { /* 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;
          // 传入 JavaMain() 函数参数,在新线程中运行 JavaMain
          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;
        }
    }
    /*
     * Block current thread and continue execution in a new thread
     */
    int
    ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
        int rslt = 0;
        unsigned thread_id;
    #ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
    #define STACK_SIZE_PARAM_IS_A_RESERVATION  (0x10000)
    #endif
        /*
         * STACK_SIZE_PARAM_IS_A_RESERVATION is what we want, but it's not
         * supported on older version of Windows. Try first with the flag; and
         * if that fails try again without the flag. See MSDN document or HotSpot
         * source (os_win32.cpp) for details.
         * 调用底层内核方法,创建新线程,将JavaMain(args)作为函数调用处理。创建后立即执行
    * 如果第一次带参创建失败,则弃参重试一次
    */
    HANDLE thread_handle = (HANDLE)_beginthreadex(NULL, (unsigned)stack_size, continuation, args, STACK_SIZE_PARAM_IS_A_RESERVATION, &thread_id); if (thread_handle == NULL) { thread_handle = (HANDLE)_beginthreadex(NULL, (unsigned)stack_size, continuation, args, 0, &thread_id); } /* AWT preloading (AFTER main thread start) */ #ifdef ENABLE_AWT_PRELOAD /* D3D preloading */ if (awtPreloadD3D != 0) { char *envValue; /* D3D routines checks env.var J2D_D3D if no appropriate * command line params was specified */ envValue = getenv("J2D_D3D"); if (envValue != NULL && JLI_StrCaseCmp(envValue, "false") == 0) { awtPreloadD3D = 0; } /* Test that AWT preloading isn't disabled by J2D_D3D_PRELOAD env.var */ envValue = getenv("J2D_D3D_PRELOAD"); if (envValue != NULL && JLI_StrCaseCmp(envValue, "false") == 0) { awtPreloadD3D = 0; } if (awtPreloadD3D < 0) { /* If awtPreloadD3D is still undefined (-1), test * if it is turned on by J2D_D3D_PRELOAD env.var. * By default it's turned OFF. */ awtPreloadD3D = 0; if (envValue != NULL && JLI_StrCaseCmp(envValue, "true") == 0) { awtPreloadD3D = 1; } } } if (awtPreloadD3D) { AWTPreload(D3D_PRELOAD_FUNC); } #endif /* ENABLE_AWT_PRELOAD */ if (thread_handle) {
    // 等待线程结束,并获取返回码 WaitForSingleObject(thread_handle, INFINITE); GetExitCodeThread(thread_handle,
    &rslt); CloseHandle(thread_handle); } else {
    // 如果实在是创建线程失败,则自身直接执行该方法即可 rslt
    = continuation(args); } #ifdef ENABLE_AWT_PRELOAD if (awtPreloaded) { AWTPreloadStop(); } #endif /* ENABLE_AWT_PRELOAD */ return rslt; }

      在linux中的创建线程如下:

    // solaris/bin/java_md_solinux.c
    /*
     * Block current thread and continue execution in a new thread
     */
    int
    ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
        int rslt;
    #ifdef __linux__
        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_xx 方式 创建线程
        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);
        }
    
        pthread_attr_destroy(&attr);
    #else /* ! __linux__ */
        thread_t tid;
        long flags = 0;
        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;
    }

      即相同的语义,不同平台下的各自实现而已。

      通过C语言的函数传递,从而进行调用。通过创建一个新线程,调用JavaMain(); 由javaMain处理所有java事务。而自身则只需等待JavaMain完成即可。这也符合面向过程编程思想。

    3. jvm初始化小结

      我们平时是通过 java xxx.xxx  或者 java -jar xx.jar 启动jvm, 通过这两篇文章,我们也看清了其背后的原理。就是通过解析各种参数,验证各种参数,验证jre环境,然后验证jre是否可用,最后将指定的mainClass加载出来。丢到一个新的线程中去执行,将执行权力转发给java代码,最后等待该线程完成。

      应该说事个流程没有难点,或者说一切都很理所当然。但其中的核心,都是通过加载JavaVM()去做的,也就是真正的jvm. 然后通过或者对应的几个接口方法地址,进行调用,从而完成启动任务。所以,java虽是一个启动命令,但核心并不在这里。而是在 hotspot 或其他地方。jdk 毕竟只是一个工具箱而已。jre 才是关键。

    不要害怕今日的苦,你要相信明天,更苦!
  • 相关阅读:
    分析 ajax 请求并抓取 “今日头条的街拍图”
    requests + 正则表达式 获取 ‘猫眼电影top100’。
    爬虫基础(暂缓更新)
    Git 操作笔记:分布式版本控制系统
    python补充
    python基础
    8.最佳电影聚类分析
    文本分析 笔记
    7.文档聚类
    5.词项相似度分析
  • 原文地址:https://www.cnblogs.com/yougewe/p/14400859.html
Copyright © 2020-2023  润新知