前言

在日常编码中,有了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的源码是不可少的。下载地址:

主入口方法

查看 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] = '
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);
}
}
'; /** * 检查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命令本质内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!