当前位置 博文首页 > 等你归去来:JVM系列(一):jvm启动过程速览

    等你归去来:JVM系列(一):jvm启动过程速览

    作者:等你归去来 时间:2021-02-07 10:29

      jvm是java的核心运行平台,自然是个非常复杂的系统。当然了,说jvm是个平台,实际上也是个泛称。准确的说,它是一个java虚拟机的统称,它并不指具体的某个虚拟机。所以,谈到java虚拟机时,往往我们通常说的都是一些规范性质的东西。

      那么,如果想要研究jvm是如何工作的,就不能是泛泛而谈了。我们必须要具体到某个指定的虚拟机实现,以便说清其过程。

     

    1. 说说openjdk

      因为java实际上已经被oracle控制,而oracle本身是个商业公司,所以从某种程度上说,这里的java并不是完全开源的。我们称官方的jdk为oraclejdk. 或者叫 hotspot vm.

      与此同时,社区维护了一个完全开源的版本,openjdk。这两个jdk实际上,大部分是相同的,只是维护的进度不太一样,以及版权归属不一样。

      所以,如果想研究jvm的实现,那么基于openjdk来做,是比较明智的选择。

      如果想了解openjdk是如何设计的,以及它有什么高级特性,以及各种最佳实践,那么买一本书是最佳选择。

      如果业有余力,想去了解了解源码的,那么可以到官网查看源码。openjdk8的源码地址为: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/   因为是国外网站的原因,速度不会很快。所以只是在网站上查看源码,还是有点累的。另外,没有ide的帮助,估计很少有人能够坚持下去。另外的下载地址,大家可以网上搜索下,资源总是有的,国人链接速度快。多花点心思找找。

      当然要说明的一点是:一个没有设计背景,没有框架概念的源码阅读,都是而流氓。那样的工作,就像是空中楼阁,并不让人踏实。

     

    2. 来谈谈C语言

      C语言,一般作为我们的大学入门语言,或多或少都接触过。但要说精通,可能就是很少一部分人了。但我要说的,只要学过C语言,对于大部分的程序阅读,基本上就不是问题了。

       openjdk的实现中,其核心的一部分就是使用C语言写的,当然其他很多语言也是一样的。所以,C语言相当重要,在底层的世界里。这里只是说它重要,但并不代表它就一定最厉害,即不是写C语言的GG就比写JAVA的JJ厉害了。因为,工作不分高低,语言同样。只是各有所长罢了。重点不是在这里,在于思想。

      C语言的编程几大流程:写代码(最核心)、编译、链接(最麻烦)、运行。

      当然,最核心的自然是写代码。不对,最核心的是:做设计。

      C语言中,以一个main()函数为入口,编写各种逻辑后,通过调用和控制main()方法,实现各种复杂逻辑。

      所以,要研究一个项目,首先就是要找到其入口。然后根据目的,再进行各功能实现的通路学习。

      C语言有极其灵活的语法,超级复杂的指针设计,以及各类似面向对象思想的结构体,以及随时可能操作系统获取信息的能力(各种链接)。所以,导致C语言有时确实比较难以读懂。这也是没办法的事,会很容易,精却很难。这是亘古不变的道理。是一个选择题,也是一道应用题。

      一句话,会一点,就够吃瓜群从使用了。

     

    3. openjdk的入口

      上面说到,要研究一个C项目,首要就是找到其入口。那么,openjdk的入口在哪呢?

      是在 share/bin/main.c 中,main()方法就是其入口。这个文件命名,够清晰了吧,明眼人一看就知道了。哈哈,不过一般地,我们还是需要通过查资料才知晓。

      main.c是jvm的唯一main方法入口,其中,jdk被编译出来之后,会有许多的工作箱,如jmap,jps,jstack.... 这些工具箱的入口,实际也是这个main, 只是它们包含了不同的子模块,从而达到不同工具的目的。

      main.c的内容也不多,主要它也只是一个框架,为屏蔽各系统的差异。它的存在,主要是为引入 JLI_LANCH() 方法,相当于定义自己的main()方法。

    /*
     * This file contains the main entry point into the launcher code
     * this is the only file which will be repeatedly compiled by other
     * tools. The rest of the files will be linked in.
     */
    #include "defines.h"
    #ifdef _MSC_VER
    #if _MSC_VER > 1400 && _MSC_VER < 1600
    /*
     * When building for Microsoft Windows, main has a dependency on msvcr??.dll.
     *
     * When using Visual Studio 2005 or 2008, that must be recorded in
     * the [java,javaw].exe.manifest file.
     *
     * As of VS2010 (ver=1600), the runtimes again no longer need manifests.
     *
     * Reference:
     *     C:/Program Files/Microsoft SDKs/Windows/v6.1/include/crtdefs.h
     */
    #include <crtassem.h>
    #ifdef _M_IX86
    #pragma comment(linker,"/manifestdependency:\"type='win32' "            \
            "name='" __LIBRARIES_ASSEMBLY_NAME_PREFIX ".CRT' "              \
            "version='" _CRT_ASSEMBLY_VERSION "' "                          \
            "processorArchitecture='x86' "                                  \
            "publicKeyToken='" _VC_ASSEMBLY_PUBLICKEYTOKEN "'\"")
    #endif /* _M_IX86 */
    //This may not be necessary yet for the Windows 64-bit build, but it
    //will be when that build environment is updated.  Need to test to see
    //if it is harmless:
    #ifdef _M_AMD64
    #pragma comment(linker,"/manifestdependency:\"type='win32' "            \
            "name='" __LIBRARIES_ASSEMBLY_NAME_PREFIX ".CRT' "              \
            "version='" _CRT_ASSEMBLY_VERSION "' "                          \
            "processorArchitecture='amd64' "                                \
            "publicKeyToken='" _VC_ASSEMBLY_PUBLICKEYTOKEN "'\"")
    #endif  /* _M_AMD64 */
    #endif  /* _MSC_VER > 1400 && _MSC_VER < 1600 */
    #endif  /* _MSC_VER */
    /*
     * Entry point.
     */
    // 定义入口函数,JAVAW模式下使用 WinMain(), 否则使用 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
        // windows下的参数获取
        {
            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 */
        // 各种linux平台上的参数,直接取自main入参
        margc = argc;
        margv = argv;
    #endif /* WIN32 */
        // 核心: 重新定义入口方法为: JLI_Launch()
        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);
    }

      因为java语言被设计成跨平台的语言,那么如何跨平台呢?因为平台差异总是存在的,如果语言本身不关注平台,那么自然是有人在背后关注了平台,从而屏蔽掉了差异。是了,这就是虚拟机存在的意义。因此,在入口方法,我们就可以看到,它一上来就关注平台差异性。这是必须的。

     

    4. openjdk的启动流程

      有了上面的入口知识,好像是明白了一些道理。但是好像还是没有达到要理解启动过程的目的。不急,且听我慢慢道来。

      我们启动一个虚拟机时,一般是使用  java -classpath:xxx <other-options> xx.xx , 或者是 java -jar <other-options> xx.jar 。 具体怎么用无所谓,重点是我们都是 java这个应用程序启动的虚拟机。因此,我们便知道 java 程序,是我们启动jvm的核心开关。

     

    4.0. jvm启动流程框架

      废话不多说,java.c, 是我们要研究的重要文件。它将是一个控制启动流程的实现超人。而它的入口,就是在main()中的定义 JLI_Launch(...) , 所以让我们一睹真容。

    // 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:\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();
        // 初始化jvm,即加载java程序开始,应用表演时间到
        return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
    }

      以上就是整个jvm虚拟机的启动过程框架了,基本上跑不掉几个点,就是解析命令行参数,设置参数到某范围内或者环境变量中。加载必要模块,传递变量存储。初始化系统。解析用户系统实现。当然一般地,就是会实现系统主循环,这个动作是由使用系统完成的,jvm只负责执行即可。

      因为我们只是想了解大概,所以不以为然,只是其中任何一个点都足够研究很久很久了。抛开那些不说,捡个芝麻先。需要明白:懂得许多的道理却依然过不好这一生。只能安心做个吃瓜群众。