关于java命令的本质逻辑揭秘过程
作者:tera
前言
在日常编码中,有了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命令本质内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!