Jvm(jdk8)源码分析1-java命令启动流程详解

1.概述

    现在大多数互联网公司都是使用java技术体系搭建自己的系统,所以对java开发工程师以及java系统架构师的需求非常的多,虽然普遍的要求都是需要熟悉各种java开发框架(如目前比较流行ssi或者ssh框架),但是对于java语言本身的理解才是本质。如果你熟悉jvm原理以及jdk本身的实现,我相信对于其他开发框架的学习和深入理解应该不是很困难,因为很多灵活和高大山的框架都使用了jdk最核心的功能。除了本身框架的使用之外,凡是使用java语言开发的系统都避免不了对jvm的调优(对于系统性能要求不高可能不需要,但是对于互联网公司来说性能好像是对系统的基本要求)。如果能够深入掌握jvm原理,对于调优jvm和解决各种java相关问题是很有帮助的,当然写的java代码自然质量是很高的。

虽然我以前使用java进行编码的时间很少,对很多java的高级功能也不是很熟悉,对于jvm原理和调优也是一知半解,但是这不影响我对jvm本身原理及代码实现的学习和研究。以前研究和学习linux的源代码就觉得其乐无穷,相信现在研究jvm的源码应该也有同样的感受,并且将有非常大的收获。

正好现在java 8已经推出,业界对java8也是比较满意。作为自己学习和研究完全就可以从java8开始了,直接通过hg工具(类似git)下载jdk8的源代码进行研究学习:hg clone http://hg.openjdk.java.net/jdk8/jdk8。下载源码以后就可以开始编译了,具体请查看帮助文档吧。编译完成以后就可以运行java或者javac等相关命令了。

2.Java启动

    在学习源码的时候,首先需要找到程序入口函数main,但是由于源代码太庞大而且可能有多个main函数,那么怎么可以快速的找到真正的入口main函数呢?这里在linux就可以借助调试工具gdb了。例如我们要快速找到java的启动入口函数,首先执行下面的命令gdb ./java会出现如下的信息:

GNU gdb (Ubuntu 7.8-1ubuntu4) 7.8.0.20141001-cvs

Copyright (C) 2014 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type “show copying”

and “show warranty” for details.

This GDB was configured as “x86_64-linux-gnu”.

Type “show configuration” for configuration details.

For bug reporting instructions, please see:

<http://www.gnu.org/software/gdb/bugs/>.

Find the GDB manual and other documentation resources online at:

<http://www.gnu.org/software/gdb/documentation/>.

For help, type “help”.

Type “apropos word” to search for commands related to “word”…

Reading symbols from ./java…done.

(gdb) 

然后就进入了gdb的命令行了,这个时候使用l命令就可以看到启动文件的代码了,如下:

(gdb) l

80 char **__initenv;

81

82 int WINAPI

83 WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)

84 {

85     int margc;

86     char** margv;

87     const jboolean const_javaw = JNI_TRUE;

88

89     __initenv = _environ;

但是之看到这个启动文件中的开始代码,它不是第一行执行的代码,而且现在也不知道具体那个文件。不过我还是可以利用断点功能,我们都知道c语言的入口都是main函数,所以我们只需要对main进行打断点即可,相关命令和输出如下:

(gdb) b main

Breakpoint 1 at 0x4005f0: file /home/brucewoo/hg/jdk8/jdk/src/share/bin/main.c, line 94.

怎么样?现在足够明显了吗?其他程序可以采用同样的方式获得程序的入口函数在哪一个文件的哪一行。我们打开这个文件验证一下确实是。那我们就一起看看这个入口代码,如下:

#ifdef JAVAW
省略的windows平台相关的代码
#else /* JAVAW */
int main(int argc, char **argv)
{
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_FALSE;
#endif /* JAVAW */
#ifdef _WIN32
    省略的windows平台相关的代码
#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);
}

然后继续看函数JLI_Launch,它接着进行java的启动。代码如下:

static jlong threadStackSize    = 0;  /* stack size of the new thread */
static jlong maxHeapSize        = 0;  /* max heap size */
static jlong initialHeapSize    = 0;  /* inital heap size */
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];//jvm的路径
    char jrepath[MAXPATHLEN];//jre的路径
    char jvmcfg[MAXPATHLEN]; //jvm配置路径
 
    _fVersion = fullversion;
    _dVersion = dotversion;
    _launcher_name = lname;
    _program_name = pname;
    _is_java_args = javaargs;
    _wc_enabled = cpwildcard;
    _ergo_policy = ergo;
 
//Initialize platform specific settings,
//会根据_JAVA_LAUNCHER_DEBUG环境变量是否设置来设置是否打印debug信息
    InitLauncher(javaw);
    DumpState();//根据是否设置debug来选择输出一些配置信息
    if (JLI_IsTraceLauncher()) {//同样如果设置了debug信息就输出命令行参数的输出
        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);//选择运行时jre的版本,规则看上面注释
 
//创建执行的环境变量
    CreateExecutionEnvironment(&argc, &argv, jrepath, sizeof(jrepath),
                               jvmpath, sizeof(jvmpath), jvmcfg,  sizeof(jvmcfg));
 
    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:选择jre的版本,这个函数实现的功能比较简单,就是选择正确的jre版本来作为即将运行java程序的版本。选择的方式,如果环境变量设置了_JAVA_VERSION_SET,那么代表已经选择了jre的版本,不再进行选择;否则,根据运行时给定的参数来搜索不同的目录选择,例如指定版本和限制了搜索目录等,也可能执行的是一个jar文件,所以需要解析manifest文件来获取相关信息,对应Manifest文件的数据结构,通过函数ParseManifest解析,具体请看下面注释。

/*

 * Information returned from the Manifest file by the ParseManifest() routine.

 * Certainly (much) more could be returned, but this is the information

 * currently of interest to the C based Java utilities (particularly the

 * Java launcher).

 */

typedef struct manifest_info {  /* Interesting fields from the Manifest */

char        *manifest_version;      /* Manifest-Version string */

char        *main_class;            /* Main-Class entry */

char        *jre_version;           /* Appropriate J2SE release spec */

char jre_restrict_search;    /* Restricted JRE search */

char        *splashscreen_image_file_name; /* splashscreen image file */

} manifest_info;

最终会解析出一个真正需要的jre版本并且判断当前执行本java程序的jre版本是不是和这个版本一样,如果不一样调用linux的execv函数终止当前进出并且使用新的jre版本重新运行这个java程序,但是进程ID不会改变。

(2)CreateExecutionEnvironment,这个函数主要创建执行的一些环境,这个环境主要是指jvm的环境,例如需要确定数据模型,是32位还是64位以及jvm本身的一些配置在jvm.cfg文件中读取和解析。里面有一个重要的函数就是专门解析jvm.cfg的,如下:jint ReadKnownVMs(const char *jvmCfgName, jboolean speculative)。这个函数解析jvm.cfg文件来确定jvm的类型,jvm的类型有如下几种(是一个枚举定义):

/* Values for vmdesc.flag */

enum vmdesc_flag {

VM_UNKNOWN = -1,

VM_KNOWN,

VM_ALIASED_TO,

VM_WARN,

VM_ERROR,

VM_IF_SERVER_CLASS,

VM_IGNORE

};

然后还有一个结构体专门描述jvm的信息,如下:

struct vmdesc {

char *name;//名字

int flag;//上面的枚举定义类型

char *alias;//别名

char *server_class;//服务器类

};

总结:这个函数主要就是确定一下jvm的信息并且初始化相关信息,为后面的jvm执行准备环境。

(3)LoadJavaVM:动态加载jvm.so这个共享库,并把jvm.so中的相关函数导出并且初始化,例如JNI_CreateJavaVM函数。后期启动真正的java虚拟就是通过这里面加载的函数,里面重要的代码如下:

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”);

总结:这个函数就是初始化jvm相关的初始化函数和入后函数,后面就是调用这里的JNI_CreateJavaVM函数真正的开始启动一个jvm的,这个函数会做很多的初始化工作,基本上一个完整的jvm信息在这个函数里面都能够看到,后面单独详细讲解这个函数。

(4)ParseArguments:解析命令行参数,就不多解析了,不同的命令行参数具体使用到来详细介绍其作用。

(5)JVMInit:这是启动流程最后执行的一个函数,如果这个函数返回了那么这个java启动就结束了,所有这个函数最终会以某种形式进行执行下去。具体先看看这个函数的主要流程,如下:

JVMInit->ContinueInNewThread->ContinueInNewThread0->(可能是新线程的入口函数进行执行,新线程创建失败就在原来的线程继续支持这个函数)JavaMain->InitializeJVM(初始化jvm,这个函数调用jvm.so里面导出的CreateJavaVM函数创建jvm了,JNI_CreateJavaVM这个函数很复杂)->LoadMainClass(这个函数就是找到我们真正java程序的入口类,就是我们开发应用程序带有main函数的类)->GetApplicationClass->后面就是调用环境类的工具获得main函数并且传递参数调用main函数,查找main和调用main函数都是使用类似java里面支持的反射实现的。

到此java这个启动命令全部流程解析完毕,但是其中还有很重要的两个流程没有分析。一个就是初始化和启动真正的jvm,由动态链接库jvm.so中的JNI_CreateJavaVM实现,另外一个就是最后查找入口类以及查找main入口函数的具体实现。这两个都涉及到很多的内容,后面会分别单独一篇文章来分析。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏维C果糖

手把手教你设置 IntelliJ IDEA 的彩色代码主题

在选完我们中意的主题之后,需要大家到「intellij-idea-tutorial」中下载相应的主题。以「SublimeMonoKai」主题为例:

8347
来自专栏Java 源码分析

Netty 入门

1. 粘包问题 一 .长连接与短连接: 1.长连接:Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。长连接在 net...

3857
来自专栏微信终端开发团队的专栏

MMKV for Android 多进程设计与实现

MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 ...

3571
来自专栏游戏杂谈

关于seajs

虽然已经有很长时间没写JavaScript,但很多时候看到一些应用还是会带着好奇心去研究一下。之前是看腾讯的朋友网,它的webchat做的很不错(虽然ff下有b...

2933
来自专栏黑白安全

PHP代码审计入门之路

虽然市面上的代码审计的文章已经一大把了,但是还是决定重复造轮子,打算作为一个系列来写的,近年越来越多的安全研究人员投入到php应用的漏洞挖掘,相对应的代码安全问...

982
来自专栏阮一峰的网络日志

处理Apache日志的Bash脚本

去年一年,我写了将近100篇网络日志。 现在这一年结束了,我要统计"访问量排名",看看哪些文章最受欢迎。(隆重预告:本文结尾处将揭晓前5名。) ? 以往,我用的...

3645
来自专栏Java 源码分析

Netty 入门

1. 粘包问题 一 .长连接与短连接: 1.长连接:Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。长连接在 net...

3005
来自专栏Java帮帮-微信公众号-技术文章全总结

大文件拆分方案的Java实践【面试+工作】

大文件拆分问题涉及到io处理、并发编程、生产者/消费者模式的理解,是一个很好的综合应用场景,为此,花点时间做一些实践,对相关的知识做一次梳理和集成,总结一些共性...

2984
来自专栏aCloudDeveloper

Linux探秘之I/O效率

一、文章来由   最近看了《UNIX环境高级编程》,对以前比较模糊的一些知识结构又做了进一步的加强,特别是前两章讲到不带缓冲的文件I/O和带缓冲的标准I/O,对...

2317
来自专栏安恒网络空间安全讲武堂

MOCTF WEB 题解

0x00 MOCTF平台是CodeMonster和Mokirin这两支CTF战队所搭建的一个CTF在线答题系统。网址是http://www.moctf.com/...

7779

扫码关注云+社区

领取腾讯云代金券