前言

在日常编码中,有了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. @Transactional+@Autowired出现的lateinit property xx has not been initialized错误

    1 问题描述 用Kotlin编写Spring Boot,在业务层中使用@Transactional+@Autowired时出现如下错误: lateinit property dao has not b ...

  2. vim与系统剪贴版的交互

    1 概述 vim中的复制,删除,替换(d,r,s,x,y等)的内容都会被保存到默认的未命名的寄存器中,之后可以通过p进行粘贴,但是,这个寄存器不是系统的剪贴版,很多时候需要vim与系统剪贴版的交互,那 ...

  3. Linux入门之基本的概念、安装和操作

    目录 Linux基本概念 Linux的安装 虚拟机安装CentOS7 CentOS设置网络 Linux基本操作命令 文件目录操作命令 进程操作命令 文本操作命令 Linux权限操作 用户和组操作命令 ...

  4. Linux 文件系统和目录结构

    1. Linux 文件系统 2. linux 目录结构 3. 磁盘分区.文件系统和目录的关系 1. Linux 文件系统 Linux 支持多种的文件系统种类,除了 linux 通常使用的 ext 系列 ...

  5. BLDC有感FOC算法理论及其STM32软硬件实现

    位置传感器:旋转编码器          MCU:STM32F405RGT6          功率MOS驱动芯片:DRV8301 全文均假设在无弱磁控制的情况下 FOC算法理论 首先,我们要知道FO ...

  6. MSF和CobaltStrike联动

    目录 当获取了CobaltStrike类型的session后,想派生一个MSF类型的shell

  7. docker文件系统分层存储原理

    一,前言 众所周知,docker镜像技术的基础是联合文件系统(UnionFS),其文件系统是分层的,那它的分层机制是什么样的呢?共分为几种层呢?又是怎么工作的呢? 目前docker支持的联合文件系统有 ...

  8. HarmonyOS三方件开发指南(19)-BGABadgeView徽章组件

    目录: 1.引言 2.功能介绍 3.BGABadgeView 使用指南 4.BGABadgeView 开发指南 5.<HarmonyOS三方件开发指南>系列文章合集 引言 现在很多的APP ...

  9. Mac/Win录屏工具推荐-LICEcap

    轻小.便捷.操作简单 下载 LICEcap v1.30 for macOS LICEcap v1.28 for Windows 参考地址

  10. solidworks中 toolbox调用出现未配置的解决方法

    解决步骤:1:win7卸载安全补丁:KB3072630 WIN10,忽略.2:关闭所有Solidworks的进程3:CMD命令行进入:cd c:\program files\solidwokrs co ...