美文网首页
JVM源码分析(一) -- java启动流程

JVM源码分析(一) -- java启动流程

作者: msrpp | 来源:发表于2018-09-30 22:10 被阅读102次

    准备工作

    首先需要下好jdk源码,我选择的是jdk8,编译过程略去。

    经过configure,make之后。在jdk8u-dev/build/macosx-x86_64-normal-server-release/jdk/bin目录会生成可执行文件。java,javac,jstack等工具,这个过程花了十几分钟。

    先准备一份java编写的程序用作调试,编写HelloWorld.java

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    public class HelloWorld {
        public HelloWorld() {
        }
    
        public static void main(String[] args) {
            System.out.println("HelloWorld!");
        }
    }
    

    编成class

    localhost:tmp jjchen$ javac HelloWorld.java
    
    

    编写 MANIFEST.MF文件确定程序执行入口,将helloworld打成jar包

    // file META-INF/MANIFEST.MF
    Manifest-Version: 1.0
    Created-By: 1.8.0_171 (Oracle Corporation)
    Main-Class: HelloWorld
    
    
    localhost:tmp jjchen$ jar cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorld.class //注意MF文件要放到前面,否则打包不了
    

    流程分析

    查看源码分析一下运行java程序之后做了什么事。命令行

    ./java -jar -Xms3550m -XstartOnFirstThread HelloWorld.jar
    

    程序入口在jdk8u-dev/jdk/src/share/bin/main.c,查看main函数。

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

    非win编译环境,什么都没干,查看JLI_Launch函数,位于jdk8u-dev/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 */
    )
    {
        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.)
         */
        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();
        }
    
        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);
    }
    

    我们需要特别关心以下这几个步骤

    1.SelectVersion

    解析部分命令行参数,主要是判断用户是以jar包方式运行(-jar)还是以class方式运行,并读取路径(-classpath/-cp),默认为环境变量配置的当前目录。如果是以jar包方式加载的,那么还会去读取META-INF/MANIFEST.MF文件,读入以下参数:Manifest-Version,Main-Class,JRE-Version,JRE-Restrict-Search,Splashscreen-Image ,除了Main-Class是程序入口以外,JRE-Restrict-Search 是jvm的查找路径,其他含义先不了解了。

    2.CreateExecutionEnvironment
    • 1. ReadKnownVMs函数
      获取jvm.cfg的一些参数

    • 2. MacOSXStartup函数

    开启了一个线程重新执行了main方法,同时将主线程挂起(。。。这个是干嘛)

    4.LoadJavaVM

    新线程继承主线程的遗志,加载jvm动态库,并加载了该动态库导出的三个方法,JNI_CreateJavaVMJNI_GetDefaultJavaVMInitArgsJNI_GetCreatedJavaVMs。这个比较重要,贴一下代码。

    jboolean
    LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
    {
        Dl_info dlinfo;
        void *libjvm;
    
        JLI_TraceLauncher("JVM path is %s\n", jvmpath);
    
        libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
        if (libjvm == NULL) {
            JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);
            JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
            return JNI_FALSE;
        }
    
        ifn->CreateJavaVM = (CreateJavaVM_t)
            dlsym(libjvm, "JNI_CreateJavaVM");
        if (ifn->CreateJavaVM == NULL) {
            JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
            return JNI_FALSE;
        }
    
        ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
            dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
        if (ifn->GetDefaultJavaVMInitArgs == NULL) {
            JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
            return JNI_FALSE;
        }
    
        ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
        dlsym(libjvm, "JNI_GetCreatedJavaVMs");
        if (ifn->GetCreatedJavaVMs == NULL) {
            JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
            return JNI_FALSE;
        }
    
        return JNI_TRUE;
    }
    
    
    5.ParseArguments

    解析用户输入参数,最终调用AddOption方法添加一些option参数保存起来,jvm堆栈大小的配置就是这里保存的。设置初始 Java 堆大小:-Xms512m,最后的单位可以是g/t/m/k
    同样的,前缀Xmx是设置最大的堆内存,Xss是设置栈空间大小。

    void
    AddOption(char *str, void *info)
    {
        /*
         * Expand options array if needed to accommodate at least one more
         * VM option.
         */
        if (numOptions >= maxOptions) {
            if (options == 0) {
                maxOptions = 4;
                options = JLI_MemAlloc(maxOptions * sizeof(JavaVMOption));
            } else {
                JavaVMOption *tmp;
                maxOptions *= 2;
                tmp = JLI_MemAlloc(maxOptions * sizeof(JavaVMOption));
                memcpy(tmp, options, numOptions * sizeof(JavaVMOption));
                JLI_MemFree(options);
                options = tmp;
            }
        }
        options[numOptions].optionString = str;
        options[numOptions++].extraInfo = info;
    
        if (JLI_StrCCmp(str, "-Xss") == 0) {
            jlong tmp;
            if (parse_size(str + 4, &tmp)) {
                threadStackSize = tmp;
            }
        }
    
        if (JLI_StrCCmp(str, "-Xmx") == 0) {
            jlong tmp;
            if (parse_size(str + 4, &tmp)) {
                maxHeapSize = tmp;
            }
        }
    
        if (JLI_StrCCmp(str, "-Xms") == 0) {
            jlong tmp;
            if (parse_size(str + 4, &tmp)) {
               initialHeapSize = tmp;
            }
        }
    }
    
    
    6.JVM初始化及运行

    先是初始化:

    int
    JVMInit(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;
                    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);
        }
    }
    

    正常情况下是调用ContinueInNewThread,然后创建一个新的线程调用java.cJavaMain函数。 当然,如果我们在启动参数里面加了-XstartOnFirstThread, sameThread会被置位,唤醒主线程来调用JavaMain(这段代码是什么语法没看懂)。

    我们进一步查看JavaMain的实现。

    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);
        }
    
        if (showSettings != NULL) {
            ShowSettings(env, showSettings);
            CHECK_EXCEPTION_LEAVE(1);
        }
    
        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.
         */
        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);
        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.
         */
        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();
    }
    
    6.1 创建虚拟机

    InitializeJVM调用JNI_CreateJavaVM,传入初始参数创建一个虚拟机,并返回了一个JNIEnv对象。JNIEnv类定义在jdk8u-dev/hotspot/src/share/vm/prims/jni.h中。CreateJavaVM的实现在对应的jni.cpp中,
    粘贴一下实现

    _JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
    #ifndef USDT2
      HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args);
    #else /* USDT2 */
      HOTSPOT_JNI_CREATEJAVAVM_ENTRY(
                                     (void **) vm, penv, args);
    #endif /* USDT2 */
    
      jint result = JNI_ERR;
      DT_RETURN_MARK(CreateJavaVM, jint, (const jint&)result);
    
      // We're about to use Atomic::xchg for synchronization.  Some Zero
      // platforms use the GCC builtin __sync_lock_test_and_set for this,
      // but __sync_lock_test_and_set is not guaranteed to do what we want
      // on all architectures.  So we check it works before relying on it.
    #if defined(ZERO) && defined(ASSERT)
      {
        jint a = 0xcafebabe;
        jint b = Atomic::xchg(0xdeadbeef, &a);
        void *c = &a;
        void *d = Atomic::xchg_ptr(&b, &c);
        assert(a == (jint) 0xdeadbeef && b == (jint) 0xcafebabe, "Atomic::xchg() works");
        assert(c == &b && d == &a, "Atomic::xchg_ptr() works");
      }
    #endif // ZERO && ASSERT
    
      // At the moment it's only possible to have one Java VM,
      // since some of the runtime state is in global variables.
    
      // We cannot use our mutex locks here, since they only work on
      // Threads. We do an atomic compare and exchange to ensure only
      // one thread can call this method at a time
    
      // We use Atomic::xchg rather than Atomic::add/dec since on some platforms
      // the add/dec implementations are dependent on whether we are running
      // on a multiprocessor, and at this stage of initialization the os::is_MP
      // function used to determine this will always return false. Atomic::xchg
      // does not have this problem.
      if (Atomic::xchg(1, &vm_created) == 1) {
        return JNI_EEXIST;   // already created, or create attempt in progress
      }
      if (Atomic::xchg(0, &safe_to_recreate_vm) == 0) {
        return JNI_ERR;  // someone tried and failed and retry not allowed.
      }
    
      assert(vm_created == 1, "vm_created is true during the creation");
    
      /**
       * Certain errors during initialization are recoverable and do not
       * prevent this method from being called again at a later time
       * (perhaps with different arguments).  However, at a certain
       * point during initialization if an error occurs we cannot allow
       * this function to be called again (or it will crash).  In those
       * situations, the 'canTryAgain' flag is set to false, which atomically
       * sets safe_to_recreate_vm to 1, such that any new call to
       * JNI_CreateJavaVM will immediately fail using the above logic.
       */
      bool can_try_again = true;
    
      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);
        }
    
        EventThreadStart event;
        if (event.should_commit()) {
          event.set_javalangthread(java_lang_Thread::thread_id(thread->threadObj()));
          event.commit();
        }
    
    #ifndef PRODUCT
      #ifndef CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED
        #define CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(f) f()
      #endif
    
        // Check if we should compile all classes on bootclasspath
        if (CompileTheWorld) ClassLoader::compile_the_world();
        if (ReplayCompiles) ciReplay::replay(thread);
    
        // Some platforms (like Win*) need a wrapper around these test
        // functions in order to properly handle error conditions.
        CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(test_error_handler);
        CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(execute_internal_vm_tests);
    #endif
    
        // Since this is not a JVM_ENTRY we have to set the thread state manually before leaving.
        ThreadStateTransition::transition_and_fence(thread, _thread_in_vm, _thread_in_native);
      } else {
        if (can_try_again) {
          // reset safe_to_recreate_vm to 1 so that retrial would be possible
          safe_to_recreate_vm = 1;
        }
    
        // Creation failed. We must reset vm_created
        *vm = 0;
        *(JNIEnv**)penv = 0;
        // reset vm_created last to avoid race condition. Use OrderAccess to
        // control both compiler and architectural-based reordering.
        OrderAccess::release_store(&vm_created, 0);
      }
    
      return result;
    }
    
    

    JNI_CreateJavaVM有效调用只有一次,已经创建过了会直接返回。
    最重要的是这句:Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);,函数体中初始化了vm环境,这个比较庞杂,后续分析。

    6.2
    • 根据MANIFEST.MF中配置的Main-Class查找出对应的类。
    • 根据对应的类找到main静态方法。
    • 调用之。

    可以看到,在调用完之后,"HelloWorld!"字样就打印出来啦。但是实际上这3个步骤是jvm动态库实现的,里面的逻辑瞄了一眼异常复杂,后续分析。

    总结

    总结一下,java程序 = java空壳子+jvm;后续开始分析jvm的功能。

    相关文章

      网友评论

          本文标题:JVM源码分析(一) -- java启动流程

          本文链接:https://www.haomeiwen.com/subject/dsbwoftx.html