前言

在日常编码中,有了ide的支持,我们已经很少直接在命令行中直接执行java XXX命令去启动一个项目了。然而我们有没有想过,一个简单的java命令背后究竟做了些什么事情?让我们看下下面几个简单的问题

1.java命令之后可以跟很多参数,那么这些参数是如何被解析的?为何-version会返回版本号而如果紧跟一个类名则会启动jvm?

2.为何我们自己定义的入口方法必须满足如下的签名?是否还有其他可能性?

public static void main(String[] args) {
}

3.如果我们需要调用自己写的native方法,必须显式地通过 System.loadLibrary() 加载动态链接库。而如果我们查看java的基础类(Thread、Object、Class等,这些类中有非常多的native方法),则会发现其内部并没有调用 System.loadLibrary() 方法,而是由静态构造函数中的 registerNatives() 负责注册其它的natvie方法。

例如:Thread.java

class Thread implements Runnable {
private static native void registerNatives();
static {
registerNatives();
}
...
}

不过 registerNatives() 本身也是一个native方法,那它所在动态链接库又是何时被加载的?

问题1和问题2自不必多言,答案一定在java命令中

而对于问题3,因为Thread、Object、Class等等作为jdk的原生类,其相关的动态链接库就是jvm本身(windows系统是 jvm.dll ,linux 系统是libjvm.so,mac 系统是 libjvm.dylib),所以很容易推测其加载动态链接库的过程一定是在jvm的启动流程中。

今天我们就以上面3个问题为引子,探究一下java命令背后的本质,即jvm的启动流程

jvm的启动流程分析

既然需要分析jvm的启动流程,那么jdk和hotspot的源码是不可少的。下载地址:http://hg.openjdk.java.net/jdk8

主入口方法

查看 java.c,jdk 目录 /src/java.base/share/native/libjli,该目录会因为不同版本的jdk有不同

入口方法是 JLI_Launch ,当然其中内容很多,我们挑选其中的重点部分来看

int
JLI_Launch(args)
{
...
//创建执行环境
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));
...
//加载jvm
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
...
//解析命令行参数,例如-h,-version等等
if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
{
return(ret);
}
...
//启动jvm
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}

那么接下去就分别查看这几个主要方法的逻辑

CreateExecutionEnvironment:创建执行环境

这个方法根据操作系统的不同有不同的逻辑,下面以linux系统为例

查看 java_md_solinux.c,jdk 目录 /src/java.base/unix/native/libjli

CreateExecutionEnvironment(args) {
/**
* 获取jre的路径
*/
if (!GetJREPath(jrepath, so_jrepath, JNI_FALSE) ) {
JLI_ReportErrorMessage(JRE_ERROR1);
exit(2);
}
JLI_Snprintf(jvmcfg, so_jvmcfg, "%s%slib%s%sjvm.cfg",
jrepath, FILESEP, FILESEP, FILESEP);
/**
* 读取jvm的版本,这里是根据jre的路径,找到jvm.cfg文件
*/
if (ReadKnownVMs(jvmcfg, JNI_FALSE) < 1) {
JLI_ReportErrorMessage(CFG_ERROR7);
exit(1);
} jvmpath[0] = '\0';
/**
* 检查jvm的版本,如果命令行中有指定,那么会采用指定的jvm版本,否则使用默认的
*/
jvmtype = CheckJvmType(pargc, pargv, JNI_FALSE);
if (JLI_StrCmp(jvmtype, "ERROR") == 0) {
JLI_ReportErrorMessage(CFG_ERROR9);
exit(4);
}
/**
* 获取动态链接库的路径
*/
if (!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath, 0 )) {
JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath);
exit(4);
}
}

主要有以下几4个步骤

1.确定jre的路径

这里会优先寻找应用程序当前目录

if (GetApplicationHome(path, pathsize)) {
...
} if (GetApplicationHomeFromDll(path, pathsize)) {
...
}

2.根据jre拼接 jvm.cfg 的路径,并读取可用的jvm配置

一般 jvm.cfg 文件在 /jre/lib 中,其内容如下:

-server KNOWN
-client IGNORE

上述2行配置分别对应不同的jvm的版本,例如第一行 -server KNOWN ,那么在加载jvm动态链接库的时候就会去 /jre/lib/server 目录中寻找

3.检查jvm类型

在执行java命令的时候,可以通过命令指定jvm版本,如果没有指定,那么就采用jvm.cfg中的第一个jvm版本

i = KnownVMIndex(arg);
if (i >= 0) {
...
}
else if (JLI_StrCCmp(arg, "-XXaltjvm=") == 0 || JLI_StrCCmp(arg, "-J-XXaltjvm=") == 0) {
...
}

4.获取动态链接库的路径

根据前面检查jvm类型的结果,获取到对应的jvm动态链接库的路径,全部按照默认的话,在Mac系统中获取到的lib路径如下

路径中的server正是之前在cfg文件中读取到的-server

/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/server/libjvm.dylib

LoadJavaVM:加载jvm

查看 java_md_solinux.c,jdk 目录 /src/java.base/unix/native/libjli

jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
/**
* 加载动态链接库,这里调用的是dlopen,而不是普通的open
*/
libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
...
/**
* 将jvm中的"JNI_CreateJavaVM"方法链接到jdk的CreateJavaVM方法上
*/
ifn->CreateJavaVM = (CreateJavaVM_t)
dlsym(libjvm, "JNI_CreateJavaVM");
/**
* 调用CreateJavaVM方法
*/
if (ifn->CreateJavaVM == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
/**
* 将jvm中的"JNI_GetDefaultJavaVMInitArgs"方法链接到jdk的GetDefaultJavaVMInitArgs方法上
*/
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
/**
* 调用GetDefaultJavaVMInitArgs方法
*/
if (ifn->GetDefaultJavaVMInitArgs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
/**
* 将jvm中的"JNI_GetCreatedJavaVMs"方法链接到jdk的GetCreatedJavaVMs方法上
*/
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs");
/**
* 调用GetCreatedJavaVMs方法
*/
if (ifn->GetCreatedJavaVMs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
}

主要步骤如下:

1.加载动态链接库,也正是我们第一个问题的答案所在

dlopen方法是dynamic link open的缩写,在打开文件的同时,加载动态链接库。可以通过 man dlopen 命令查看说明

man dlopen
dlopen -- load and link a dynamic library or bundle

2.链接并调用jvm中的 JNI_CreateJavaVM 、GetDefaultJavaVMInitArgs、GetCreatedJavaVMs

dlsym方法是dynamic link symbol的缩写,将动态链接库中的方法链接到当前方法上

man dlsym
dlsym -- get address of a symbol

这3个方法顾名思义,分别是创建jvm、获取默认的jvm启动参数、获取创建完成的jvm。这3个方法的入口在

hotspot 目录 /src/share/vm/prims/jni.cpp

文件中,有兴趣的同学可以自行查看

ParseArguments:解析命令行参数

查看 java.c,jdk 目录 /src/java.base/share/native/libjli

static jboolean
ParseArguments(int *pargc, char ***pargv,
int *pmode, char **pwhat,
int *pret, const char *jrepath)
{
...
if (JLI_StrCmp(arg, "--version") == 0) {
printVersion = JNI_TRUE;
printTo = USE_STDOUT;
return JNI_TRUE;
}
...
if (JLI_StrCCmp(arg, "-ss") == 0 ||
JLI_StrCCmp(arg, "-oss") == 0 ||
JLI_StrCCmp(arg, "-ms") == 0 ||
JLI_StrCCmp(arg, "-mx") == 0) {
char *tmp = JLI_MemAlloc(JLI_StrLen(arg) + 6);
sprintf(tmp, "-X%s", arg + 1); /* skip '-' */
AddOption(tmp, NULL);
}
...
}

其中的参数一共有2大类。

1.类似于 --version 的参数在解析之后会直接返回

2.类似于 -mx、-mx 的参数则会通过 AddOption 方法添加成为 VM option

/*
* Adds a new VM option with the given name and value.
*/
void
AddOption(char *str, void *info)
{
...
}

JVMInit:启动jvm

查看 java_md_solinux.c,jdk 目录 /src/java.base/unix/native/libjli

JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
//在一个新线程中启动jvm
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}

在该方法中,会调用 ContinueInNewThread 创建一个新线程启动jvm

查看 java.c,jdk 目录 /src/java.base/share/native/libjli

int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
...
/**
* 创建一个新的线程创建jvm并调用main方法
*/
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
return (ret != 0) ? ret : rslt;
}

在该方法中,会调用 ContinueInNewThread0 并传入 JavaMain 入口方法

查看 java_md_solinux.c,jdk 目录 /src/java.base/unix/native/libjli

/**
* 阻塞当前线程,并在一个新线程中执行main方法
*/
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
//创建一个新线程执行传入的continuation,其实也就是外面传入的main方法
if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
void * tmp;
//当前线程阻塞
pthread_join(tid, &tmp);
rslt = (int)(intptr_t)tmp;
}
...
}

在该方法中,会创建一个新线程调用传入的 main 方法,而当前线程则阻塞

因为这里pthread_join是等待在运行main方法的线程上,所以java程序运行时,如果main线程运行结束了,整个进程就会结束,而由main启动的子线程对整个进程是没有影响的

查看 java.c,jdk 目录 /src/java.base/share/native/libjli

int JNICALL
JavaMain(void * _args)
{
//启动jvm
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
...
//加载主类
mainClass = LoadMainClass(env, mode, what);
//找到main方法id
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
//通过jni回调java代码中的main方法
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
}

这里对于main方法的方法名和签名都是固定判断的,所以无论是什么java程序,入口方法必须是 public static void main(String[] args)

到此jvm从准备启动到最后执行main方法的代码流程就结束了。因为这个流程的方法分散在不同的文件中,会很让人头晕,所以我总结了成了以下结构,方便大家理解

入口方法:JLI_Launch

        |--------->创建执行环境:CreateExecutionEnvironment

        |          |--------->获取jre的路径:GetJREPath

        |          |--------->读取jvm配置:ReadKnownVMs

        |          |--------->检查jvm类型:CheckJvmType

        |          |--------->获取jvm动态链接库路径:GetJVMPath 

        |--------->加载jvm动态链接库:LoadJavaVM

        |          |--------->加载动态链接库:dlopen

        |          |--------->链接jvm方法:dlsym

        |--------->解析命令行参数:ParseArguments

        |          |--------->类似于 --version 的参数在解析之后会直接返回

        |          |--------->类似于 -mx、-mx 的参数则会通过 AddOption 方法添加成为 VM option

        |--------->启动jvm并执行main方法:JVMInit       

                   |--------->创建一个新线程并执行后续任务:ContinueInNewThread

                               |--------->创建新线程执行main方法:ContinueInNewThread0(JavaMain)

                                 			|--------->创建新线程,用于执行传入的main方法:pthread_create

                                 			|--------->阻塞当前线程:pthread_join

                               |--------->获取main方法:JavaMain

                                 			|--------->加载主类:LoadMainClass

                                 			|--------->根据签名获取main方法的id:GetStaticMethodID

                                 			|--------->执行main方法:CallStaticVoidMethod

java命令的本质逻辑揭秘的更多相关文章

  1. attilax.java 注解的本质and 使用最佳实践(3)O7

    attilax.java 注解的本质and 使用最佳实践(3)O7 1. 定义pojo 1 2. 建立注解By eclipse tps 1 3. 注解参数的可支持数据类型: 2 4. 注解处理器 2 ...

  2. Java 命令行编译项目

    如果是用Exlipse, 第三方的包可以放在eclipse文件夹的jre包的lib文件夹中! (初学者的一些总结-高手们勿喷哈-) 原因: 以前一直用Eclispe编程环境运行Java.非常舒服,就像 ...

  3. eclipse中的javac命令与java命令

    一.eclipse的javac命令:当eclipse对.java(源文件)文件进行保存操作时(快捷键ctrl+s),会执行javac命令.见上图,Default output folder(默认输出文 ...

  4. 穿越之旅之--android中如何执行java命令

    android的程序基于java开发,当我们接上调试器,执行adb shell,就可以执行linux命令,但是却并不能执行java命令. 那么在android的shell中是否就不能执行java程序了 ...

  5. 学习 java命令

    依稀记得自己第一次编译*.java文件,第一次运行*.class文件.但是六七年过去了,现在运行java写的程序更多的是用tomcat这种web容器.最近有个小需求,写一个监控zookeeper集群的 ...

  6. javac 及 java命令的使用问题(错误或无法加载主类)

    一.问题 使用 javac 命令编译完.java源文件后,用 java 命令运行.class文件时,通常会遇到如下或类似的问题: 错误: 找不到或无法加载主类 HelloWorld.class 二.解 ...

  7. 在CMD窗口中使用javac和java命令进行编译和执行带有包名的具有继承关系的类

    一.背景 最近在使用记事本编写带有包名并且有继承关系的java代码并运行时发现出现了很多错误,经过努力一一被解决,今天我们来看一下会遇见哪些问题,并给出解决办法. 二.测试过程 1.父类代码 pack ...

  8. JAVA命令大全

    1.java.exe:======================运行java程序,这个相信每一位用Java的人知道了. 2.javac.exe:======================编译的Ja ...

  9. DOS终端不能使用JAVA命令

    DOS 终端 java 命令OK javac 命令OK java version  命令OK 写了小程序HelloWorld 测试 ,文件名HelloWorld.java public class H ...

随机推荐

  1. 【Azure 环境】在Windows系统中 使用Terraform创建中国区Azure资源步骤(入门级)

    Terraform(全称:Hashicorp Terraform )是一种开源工具,用于预配和管理云基础结构. 它将基础结构编入描述云资源拓扑的配置文件中. 这些资源包括虚拟机.存储帐户和网络接口等. ...

  2. 开坑:mysql相关问题

    一. 先过滤后连表和先连表后在mysql中选择的哪一种? 二. left join 和inner join使用场景有什么区别? 三. 第二个问题的衍生问题:left join中where 条件使用对n ...

  3. CPF 入门教程 - 设计器和模板库的使用(五)

    CPF netcore跨平台UI框架 系列教程 CPF 入门教程(一) CPF 入门教程 - 数据绑定和命令绑定(二) CPF 入门教程 - 样式和动画(三) CPF 入门教程 - 绘图(四) CPF ...

  4. ACM JAVA大数

    有的水题自己模拟下大数就过了,有的各种坑,天知道曾经因为大数wa了多少次....自己最近学者用JAVA,下面是自己总结的JAVA常用知识.. 框架 import java.util.Scanner; ...

  5. 在Android的App中动态的加载Java类

    原文的地址:http://yenliangl.blogspot.com/2009/11/dynamic-loading-of-classes-in-your.html 我正在编写一个应用程序能够加载别 ...

  6. POJ 2135 简单费用流

    题意:       题意是一个人他要从牧场1走到牧场n然后在走回来,每条路径只走一次,问全程的最短路径是多少. 思路:        这个题目挺简单的吧,首先要保证每条边只能走一次,然后还要要求费用最 ...

  7. POJ1151基本的扫描线求面积

    题意:      给定n个矩形的对角坐标,分别是左下和右上,浮点型,求矩形覆盖的面积. 思路:       基本的线段树扫描线求面积,没有坑点,不解释了,提示一点,有的题尤其是线段树扫描线的题需要离散 ...

  8. Windows核心编程 第四章 进程(下)

    4.3 终止进程的运行 若要终止进程的运行,可以使用下面四种方法: • 主线程的进入点函数返回(最好使用这个方法) . • 进程中的一个线程调用E x i t P r o c e s s函数(应该避免 ...

  9. 一文详解MySQL如何同时自增自减多个字段

    本文将带大家聊一下如何同时自增自减多个字段 开始之前,先分享一套MySQL教程,小白入门或者学习巩固都可以看 MySQL基础入门-mysql教程-数据库实战(MySQL基础+MySQL高级+MySQL ...

  10. 全套Project版本安装教程及下载地址

    1:Project 2007 安装教程及下载地址 https://mp.weixin.qq.com/s/8iI7x1qjon0yAdo3bStjzw 2:Project 2010 安装教程及下载地址 ...