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

JAVA烂猪皮

共 21434字,需浏览 43分钟

 · 2021-02-22

走过路过不要错过

点击蓝字关注我们


上一篇粗略讲了下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 __stdcallint JNICALLJavaMain(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\n",               (long)(jint)Counter2Micros(end-start));    }    /* At this stage, argc/argv have the application's arguments */    if (JLI_IsTraceLauncher()){        int i;        printf("%s is '%s'\n", launchModeNames[mode], what);        printf("App's argc is %d\n", argc);        for (i=0; i < argc; i++) {            printf("    argv[%2d] = '%s'\n", 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 jclassLoadMainClass(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\n",               (long)(jint)Counter2Micros(end-start));        printf("----%s----\n", JLDEBUG_ENV_ENTRY);    }    return (jclass)result;}    
// 初始化jvm, 主要是调用 CreateJavaVM() 方法,进行创建jvm操作/* * Initializes the Java Virtual Machine. Also frees options array when * finished. */static jbooleanInitializeJVM(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:\n "); printf("version 0x%08lx, ", (long)args.version); printf("ignoreUnrecognized is %s, ", args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE"); printf("nOptions is %ld\n", (long)args.nOptions); for (i = 0; i < numOptions; i++) printf(" option[%2d] = '%s'\n", 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. */intJLI_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:\n");        for (i = 0; i < argc ; i++) {            printf("argv[%d] = %s\n", 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\n",             (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 threadintJVMInit(InvocationFunctions* ifn, jlong threadStackSize, int argc, char **argv, int mode, char *what, int ret) { if (sameThread) { JLI_TraceLauncher("In same thread\n"); // 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.cintJVMInit(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.cintContinueInNewThread(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 */intContinueInNewThread0(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 */intContinueInNewThread0(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 才是关键。




往期精彩推荐



腾讯、阿里、滴滴后台面试题汇总总结 — (含答案)

面试:史上最全多线程面试题 !

最新阿里内推Java后端面试题

JVM难学?那是因为你没认真看完这篇文章


END


关注作者微信公众号 —《JAVA烂猪皮》


了解更多java后端架构知识以及最新面试宝典


你点的每个好看,我都认真当成了


看完本文记得给作者点赞+在看哦~~~大家的支持,是作者源源不断出文的动力


作者:等你归去来

出处:https://www.cnblogs.com/yougewe/p/14400859.html

浏览 77
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报