关于java命令的本质逻辑揭秘过程

前言

在日常编码中,有了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命令的本质逻辑揭秘的文章就介绍到这了,更多相关java命令本质内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于java命令的本质逻辑揭秘过程

    前言 在日常编码中,有了ide的支持,我们已经很少直接在命令行中直接执行java XXX命令去启动一个项目了.然而我们有没有想过,一个简单的java命令背后究竟做了些什么事情?让我们看下下面几个简单的问题 1.java命令之后可以跟很多参数,那么这些参数是如何被解析的?为何-version会返回版本号而如果紧跟一个类名则会启动jvm? 2.为何我们自己定义的入口方法必须满足如下的签名?是否还有其他可能性? public static void main(String[] args) { } 3.

  • 详谈java命令的本质逻辑揭秘

    前言 在日常编码中,有了ide的支持,我们已经很少直接在命令行中直接执行java XXX命令去启动一个项目了.然而我们有没有想过,一个简单的java命令背后究竟做了些什么事情?让我们看下下面几个简单的问题 1.java命令之后可以跟很多参数,那么这些参数是如何被解析的?为何-version会返回版本号而如果紧跟一个类名则会启动jvm? 2.为何我们自己定义的入口方法必须满足如下的签名?是否还有其他可能性? public static void main(String[] args) { } 3.

  • 通过Spring Shell 开发 Java 命令行应用

    提到 Java,大家都会想到 Java 在服务器端应用开发中的使用.实际上,Java 在命令行应用的开发中也有一席之地.在很多情况下,相对于图形用户界面来说,命令行界面响应速度快,所占用的系统资源少.在与用户进行交互的场景比较单一时,命令行界面是更好的选择.命令行界面有其固定的交互模式.通常是由用户输入一系列的参数,在执行之后把相应的结果在控制台输出.命令行应用通常需要处理输入参数的传递和验证.输出结果的格式化等任务.Spring Shell 可以帮助简化这些常见的任务,让开发人员专注于实现应用

  • Java 下数据业务逻辑开发技术 JOOQ 和 SPL

    目录 引言 语言特征 编程风格 运行模式 外部类库 IDE和调试 学习难度 代码量 结构化数据对象 定义 读数据库 写数据库 访问字段 有序访问 结构化数据计算 改名 条件查询 分组汇总 各组前3名 某支股票最大连续上涨天数 SPL函数选项和层次参数 流程处理 应用结构 Java集成 热部署 代码移植 引言 很多开源技术都可以在Java下实现以数据库为核心的业务逻辑,其中JOOQ的计算能力比Hibernate强,可移植性比MyBatis强,受到越来越多的关注.esProc SPL是新晋的数据计算

  • 浅谈Java内存区域与对象创建过程

    一.java内存区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁.根据<Java虚拟机规范(JavaSE7版)>的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域. 1.程序计数器(线程私有) 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码

  • Java命令设计模式详解

    将来自客户端的请求传入一个对象,从而使你可用不同的请求对客户进行参数化.用于"行为请求者"与"行为实现者"解耦,可实现二者之间的松耦合,以便适应变化.分离变化与不变的因素. 一.角色 Command 定义命令的接口,声明执行的方法. ConcreteCommand 命令接口实现对象,是"虚"的实现:通常会持有接收者,并调用接收者的功能来完成命令要执行的操作. Receiver 接收者,真正执行命令的对象.任何类都可能成为一个接收者,只要它能够实现

  • 23种设计模式(17)java命令模式

    23种设计模式第十七篇:java命令模式 定义:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能. 类型:行为类模式 类图: 命令模式的结构 顾名思义,命令模式就是对命令的封装,首先来看一下命令模式类图中的基本结构: Command类:是一个抽象类,类中对需要执行的命令进行声明,一般来说要对外公布一个execute方法用来执行命令. ConcreteCommand类:Command类的实现类,对抽象类中声明的方法进行实现.

  • 通过实例解析Java class文件编译加载过程

    一.Java从编码到执行 首先我们来看一下Java是如何从编码到执行的呢? 我们有一个x.java文件通过执行javac命令可以变成x.class文件,当我们调用Java命令的时候class文件会被装载到内存中,这个过程叫做classloader.一般情况下我们自己写代码的时候会用到Java的类库,所以在加载的时候也会把Java类库相关的类也加载到内存中.装载完成之后会调用字节码解释器和JIT即时编译器来进行解释和编译,编译完之后由执行引擎开始执行,执行引擎下面对应的就是操作系统硬件了.下图是大

  • Java命令行运行错误之找不到或无法加载主类问题的解决方法

    目录 前言: 一. 问题分析 二. 问题解决 1. 类名错误 2. 类所在位置未添加至类加载路径中 三.扩展知识 1. JDK目录结构及环境变量介绍 2. 为什么jdk1.5后不需要配置环境变量了? 总结 前言: 虽然学习Java语言约有两年多,但在最近需要使用命令行工具编译并运行Java程序时,还是报错了.花费了一些时间,解决了该问题,发现解决方法在初学Java时使用过.一则,为了避免以后再出现同样的问题而浪费不必要的时间:二则,作为使用该语言的程序员,对于该语言的一些基本问题,应该有清晰的理

  • Java抽象的本质解析

    目录 Java抽象基础知识 抽象 抽象的定义 abstract的使用 抽象方法: 抽象类: 定义抽象类 抽象类的一些注意点 总结 茫茫人海千千万万,感谢这一秒你看到这里.希望我的能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! Java抽象基础知识 抽象 抽象是从众多的事物中抽取出共同的.本质性的特征,而舍弃其非本质的特征的过程.具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精.去伪存真.由此及彼.由表及里的加工制作,形成概念.判断.推理等思维形式,以反映事物

随机推荐