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

时间:2024-01-20 21:25:03

JDK8加载源码分析

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 startend;

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入口函数的具体实现。这两个都涉及到很多的内容,后面会分别单独一篇文章来分析。

【源】:https://blog.csdn.net/qiangweiloveforever/article/details/51810294