0. 前言
一个类被编译为class file之后,使用java命令去执行,暂时抛开OS层面的syscall 及 glibc的入口函数, java中的main方法执行,经历了什么样的过程?要执行main方法,必须要有vm支持,那vm又是如何去构建的?
本章我们把握整体流程,建立一个初步认识。
1. 整体流程
1.main() main.c
1.JLI_Launch() java.c
1.1 CreateExecutionEnvironment() java_md_solinux.c
SetExecname() java_md_solinux.c
1.2 SetJvmEnvironment() java.c
1.3 LoadJavaVM() java_md_solinux.c 通过syscall加载libjvm.so 或 jvm.dll
1.4 SetClassPath(..) 设置目标为class文件或jar包路径
1.5 JvmInit() java_md_solinux.c
1.5.1 ContinueInNewThread() java.c
1.5.2 ContinueInNewThread0() java_md_solinux.c, 创建并等待“main线程"运行结束。java栈的大小在linux64位下默认1024k,32位默认320k.
2.JavaMain() java.c main线程的入口
2.1 InitializeJVM() java.c 初始化vm
JNI_CreateJavaVM() jni.cpp
create_vm() thread.cpp 创建vm的主要工作都在这个函数里完成
2.2 LoadMainClass() 加载main class
2.3 CallStaticVoidMethod() 真正去执行java类中的main函数
2. main() main.c - 一切从这里开始
使用如下命令即可在Linux系统的shell环境下启动Java程序
java [options] xxx.class param1 param2 ... paramn
当执行命令时,shell会创建新进程来执行该命令,由于JVM是用C/C++实现的,我们只需定位到main函数即可。至于glibc的入口函数,此处不再赘述。
main.c - main(..) / WinMain(..)
openjdk-jdk8u/jdk/src/share/bin/main.c
openjdk-jdk8u/jdk/src/share/bin/main.c
// 这里我们忽略掉源代码中关于windows部分的编译选项
int main(int argc, char **argv)
{
int margc; // 命令参数个数,从java开始数,空格隔开,假设命令是:java xxx.class 1 2 3,那么此时margc = 5
char** margv; // 参数指针
const jboolean const_javaw = JNI_FALSE;
margc = argc;
margv = argv;
// 调用java.c的JLI_Launch函数
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), // const_jargs字符串个数
const_jargs, // 编译选项[JAVA_ARGS],默认为空,一般只有java tools编译时使用JAVA_ARGS
sizeof(const_appclasspath) / sizeof(char *), // const_appclasspath的个数
const_appclasspath, // 1. 编译选项[JAVA_ARGS][APP_CLASSPATH],为APP_CLASSPATH选项的值
// 2. 只有编译选项[JAVA_ARGS], 默认为{ "/lib/tools.jar", "/classes" }
// 3. 为空
FULL_VERSION, // 编译选项[DFULL_VERSION,编译时必须填,否则编译不过
DOT_VERSION, // 编译选项 [JDK_MAJOR_VERSION] "." [JDK_MINOR_VERSION] 组成,编译时必须填,否则不通过
(const_progname != NULL) ? const_progname : *margv, // 编译选项[JAVA_ARGS] 或 [PROGNAME]确定,这里是java
(const_launcher != NULL) ? const_launcher : *margv, // 编译选项[LAUNCHER_NAME],这里是java
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE, // 在java 这里是false
const_cpwildcard, // 编译选项[EXPAND_CLASSPATH_WILDCARDS]决定,定义EXPAND_CLASSPATH_WILDCARDS 时为true,否则为false。 这里是true
const_javaw, // 编译选项[JAVAW决定,windows]下为true,linux下为false。
const_ergo_class);// 编译选项[NEVER_ACT_AS_SERVER_CLASS_MACHINE] / [ALWAYS_ACT_AS_SERVER_CLASS_MACHINE],决定ergo_policy,可以决定jvm运行哪种模式下
}
TIP:JDK编译选项
编译选项
在JDK的bin目录下,几乎所有的命令程序 都是通过同一个main函数编译生成的。
这些命令包括java、javac、jconsole、jps、jmap和jhat等。
根据编译时选项的不同,这些命令在执行时会进行不同的处理。具体的编译配置信息可参见附录章节。
$(eval $(call SetupLauncher,
java, \ #1 launcher 名称
-DEXPAND_CLASSPATH_WILDCARDS,\ #2 CFLAGS
, \ #3 LDFLAGS
, \ #4 LDFLAGS_SUFFIX_posix
user32.lib comctl32.lib, \ #5 LDFLAGS_SUFFIX_windows
$(JDK_OUTPUTDIR)/objs/jli_static.lib,\ #6 optional Windows JLI library (full path) : windows jli库的全路径
$(JAVA_RC_FLAGS), \ #7 optional Windows resource (RC) flags : windows 资源文件标识
$(JDK_TOPDIR)/src/windows/resource/java.rc,\ #8 optional Windows version resource file (.rc) : windows 版本资源文件(.rc)
$(JDK_OUTPUTDIR)/objs/java_objs,\ #9 different output dir : 不同的输出目录
true #10 if set, link statically with c runtime on windows. : 如果设置参数10 , 静态链接c运行时,在windows上.
)
)
# jmap
$(eval $(call SetupLauncher,
jmap,
-DJAVA_ARGS='{ "-J-ms8m"$(COMMA)
"-J-Dsun.jvm.hotspot.debugger.useProcDebugger"$(COMMA)
"-J-Dsun.jvm.hotspot.debugger.useWindbgDebugger"$(COMMA)
"sun.tools.jmap.JMap"$(COMMA) }'
-DAPP_CLASSPATH='{ "/lib/tools.jar"$(COMMA) "/lib/sa-jdi.jar"$(COMMA) "/classes" }' ,
,,,,,,,,
Info-privileged.plist)) # Parameter 11 if set, override plist file on macosx. : 如果设置,覆盖 plist文件在 macosx上
// windows 上的java
ifeq ($(OPENJDK_TARGET_OS), windows)
$(eval $(call SetupLauncher,javaw,
-DJAVAW -DEXPAND_CLASSPATH_WILDCARDS,,,user32.lib comctl32.lib,
$(JDK_OUTPUTDIR)/objs/jli_static.lib, $(JAVA_RC_FLAGS),
$(JDK_TOPDIR)/src/windows/resource/java.rc,,true))
endif
FULL_VERSION 和 DOT_VERSION
分别表示jdk的版本的2种表现形式,看图示
JLI_Launch() java.c
openjdk-jdk8u/jdk/src/share/bin/java.c
jdk/src/share/bin/java.c
int
JLI_Launch(int argc, char ** argv, /* main argc, argc 主参数 */
int jargc, const char** jargv, /* JAVA_ARGS 编译时设置的参数 */
int appclassc, const char** appclassv, /* app classpath */
const char* fullversion, /* full version defined */
const char* dotversion, /* dot version defined */
const char* pname, /* program name 这里是"java"*/
const char* lname, /* launcher name 这里是"java"*/
jboolean javaargs, /* 有无编译选项JAVA_ARGS, 这里是false*/
jboolean cpwildcard, /* classpath wildcard,java时,这里为true,其它工具时,大部分为false*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy,java时这里0,default选项*/
)
{
int mode = LM_UNKNOWN;
char *what = NULL;
char *cpath = 0;
char *main_class = NULL;
int ret;
InvocationFunctions ifn; // 用来保存将要调用的相关函数指针
jlong start = 0, end = 0;
char jvmpath[MAXPATHLEN];
char jrepath[MAXPATHLEN];
char jvmcfg[MAXPATHLEN];
// 静态变量赋值
_fVersion = fullversion;
_dVersion = dotversion;
_launcher_name = lname;
_program_name = pname;
_is_java_args = javaargs;
_wc_enabled = cpwildcard;
_ergo_policy = ergo;
/* Initialize platform specific settings
只有在windows下,编初始MFC窗体,根据环境变量_JAVA_LAUNCHER_DEBUG的取值决定是否输出JVM Trace信息,
若_JAVA_LAUNCHER_DEBUG==1,输出JVM Trace信息,同时设置全局变量_launcher_debug = true
*/
InitLauncher(javaw);
// 打印Launcher状态信息
DumpState();
if (JLI_IsTraceLauncher()) {
int i;
printf("Command line args:\n");
for (i = 0; i < argc ; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
AddOption("-Dsun.java.launcher.diag=true", NULL);
}
/*
校验JRE version是否有效,可略过不看
1. 以jar方式包运行时 , 加载Manifest判断JRE Version是否有效
2. 以指定版本运行时,校验JRE Version是否存在。
可以在Java命令中指定特定版本的Java,命令:java -version:<major>.<minor>.<security>
例如:java -version:1.8 HelloWorld
*/
SelectVersion(argc, argv, &main_class);
-------------------------------------------1. CreateExecutionEnvironment
/*
创建执行环境,主要做以下几件事
1、处理一下参数设置,比如按32位运行还64位运行
2、处理jrepath、jvmpath、jvmcfg(jvm的自身配置),后续有说明
3、设置执行程序路径:/xxx/jdk/bin/java
*/
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath), jvmpath, sizeof(jvmpath), jvmcfg, sizeof(jvmcfg));
-------------------------------------------2. SetJvmEnvironment
if (!IsJavaArgs()) { // 像jmap、jinfo、jhat 等编译时设置编译选项【JAVA_ARGS时】,以设置main class位置,而java不设此选项
// 根据是否配置命令行参数:-XX:NativeMemoryTracking=[off | summary | detail]
// 决定是否设置环境变量:NMT_LEVEL_${PID}=value,用以跟踪内存使用情况
SetJvmEnvironment(argc,argv);
}
ifn.CreateJavaVM = 0; // JNI_CreateJavaVM函数指针
ifn.GetDefaultJavaVMInitArgs = 0; // JNI_GetDefaultJavaVMInitArgs函数指针
if (JLI_IsTraceLauncher()) {
start = CounterGet();
}
-------------------------------------------3. LoadJavaVM
/* 加载VM,可以理解为java、javac 都是壳子,真正vm代码在动态链接libjvm.so(在windows下是jvm.dll)里面的几个函数(
CreateJavaVM->JNI_CreateJavaVM、
GetDefaultJavaVMInitArgs->JNI_GetDefaultJavaVMInitArgs、
GetCreatedJavaVMs->JNI_GetCreatedJavaVMs),
拿到函数指针,并由ifn来持有
*/
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
if (JLI_IsTraceLauncher()) {
end = CounterGet();
}
JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n",
(long)(jint)Counter2Micros(end-start));
// 移除命令行第一条命令:argv[0] = C:\Tools\Java\open-jre1.8.0_202\bin\java.exe
++argv;
--argc;
if (IsJavaArgs()) {
/* 一般只有java tools才会进入这里, 以javac的编译配置为例
$(eval $(call SetupLauncher,javac, \
-DEXPAND_CLASSPATH_WILDCARDS -DNEVER_ACT_AS_SERVER_CLASS_MACHINE \
-DJAVA_ARGS='{ "-J-ms8m"$(COMMA) "com.sun.tools.javac.Main"$(COMMA) }'))
*/
/* For tools, convert command line args thus:
* javac -cp foo:foo/"*" -J-ms32m ...
* java -ms32m -cp JLI_WildcardExpandClasspath(foo:foo/"*") ...
*/
TranslateApplicationArgs(jargc, jargv, &argc, &argv);
// 2. 添加可选参数:
// 1. -Denv.class.path= -> CLASSPATH
// 2. -Dapplication.home= -> APP_HOME
// 3. -Djava.class.path= -> 取决于:宏定义变量JAVA_ARGS是否存在,见附录defines.h
if (!AddApplicationOptions(appclassc, appclassv)) {
return(1);
}
} else {
// 如果环境变量:CLASSPATH 未定义,默认设置为当前目录:"."
cpath = getenv("CLASSPATH");
if (cpath == NULL) {
cpath = ".";
}
// 添加可选参数:-Djava.class.path=cpath下的所有jar文件路径(以分隔符隔开,windows下是; 其它系统下是:)
SetClassPath(cpath);
}
/*
剥离以'-'开头的参数并设置相应的jvm可选参数,参数包装成JavaVMOption,放入全局变量options(这是数组)
处理这三个参数时,会额外给这三个全局变量赋值
-Xss threadStackSize
-Xmx maxHeapSize
-Xms initialHeapSize
mode:LM_UNKNOWN | LM_CLASSPATH | LM_JAR
what:
1. 如果是jar包启动,what值为jar包路径:test.jar | D:\workdir\study\out\artifacts\test\test.jar
2. 如果是Class启动,what值为class完整名:com.johnjoe.study.Test
*/
if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
{
// 如果是 -version -fullversoin -h -? -help, 从ParseArguments出来后,就结束了
return(ret);
}
if (mode == LM_JAR) {
// 以jar方式运行,重置可选参数:-Djava.class.path=
SetClassPath(what); /* Override class path */
}
// 添加可选参数:-Dsun.java.command=${java.exe完整路径} [ ${主程序完整名} | ${jar包}] ${命令行参数}
SetJavaCommandLineProp(what, argc, argv);
//添加可选参数:-Dsun.java.launcher=SUN_STANDARD
SetJavaLauncherProp();
//(**Linux only**)添加可选参数:-Dsun.java.launcher.pid=
SetJavaLauncherPlatformProps();
// 全局变量threadStackSize初始为0,在上面解析参数时,若碰到-Xss,则会更新为用户设置的值
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
1. CreateExecutionEnvironment()
openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c
JVM初始化前置准备,创建执行环境
这个函数给jvm运行提前创建执行环境,主要做以下几件事情
1、找到执行程序的路径
2、确定执行平台的架构
3、确定执行模式:client/server
jdk/src/solaris/bin/java_md_solinux.c
void
CreateExecutionEnvironment(int *pargc, char ***pargv,
char jrepath[], jint so_jrepath,
char jvmpath[], jint so_jvmpath,
char jvmcfg[], jint so_jvmcfg) {
jboolean jvmpathExists;
/* Compute/set the name of the executable */
// 设置执行程序的路径
SetExecname(*pargv);
/* Check data model flags, and exec process, if needed */
{
// 得到linux平台下执行的架构:LIBARCH32NAME-32位机器、LIBARCH64NAME-64位机器、LIBARCHNAME-默认
char *arch = (char *)GetArch(); /* like sparc or sparcv9 */
char * jvmtype = NULL;
int argc = *pargc;
char **argv = *pargv;
int running = CURRENT_DATA_MODEL; // 表示正在执行的机器位数
int wanted = running; /* What data mode is being
asked for? Current model is
fine unless another model
is asked for */
............
}
}
SetExecname()
openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c
这个函数就是找出执行程序的路径,也就是java命令所在的路径,主要按4种方式找,下面按查找优先顺序列出
1、/proc/self/exe 链接路径
2、绝对路径
3、相对路径
4、全局搜索
jdk/src/solaris/bin/java_md_solinux.c
const char*
SetExecname(char **argv)
{
char* exec_path = NULL;
#if defined(__solaris__)
........
#elif defined(__linux__)
{
// 在linux操作系统中"/proc/self/exe"链接到当前进程的执行程序目录,
// 比如,当我们在linux操作脚本的时候,这个目录就会链接到bash脚本程序目录,
// 这里是链接到java程序,也就是我们的java命令的目录
const char* self = "/proc/self/exe";
char buf[PATH_MAX+1];
int len = readlink(self, buf, PATH_MAX);
if (len >= 0) {
buf[len] = '\0'; /* readlink(2) doesn't NUL terminate */
exec_path = JLI_StringDup(buf);
}
}
#else /* !__solaris__ && !__linux__ */
{
/* Not implemented */
}
#endif
/* 到这一步,还没拿到执行程序路径,那就再通过别的方式去找,具体方式如下:
1、如果命令是/开头的,那就按绝对路径去找
2、绝对路径找不到,那再相对路径找
3、还找不到,就通过系统搜索的方式
*/
if (exec_path == NULL) {
exec_path = FindExecName(argv[0]);
}
// 将得到的执行程序路径存储到全局变量execname中,方便后续逻辑使用
execname = exec_path;
return exec_path;
}
2. SetJvmEnvironment()
这个函数最主要的 是在于管理NativeMemoryTracking参数。该参数通过监控点记录JVM向系统申请内存时的活动,虽然有助于内存管理,但会带来一定的性能开销,因此使用时应谨慎考虑。。
NMT_LEVEL_353191=[off | summary | detail] (353191是进程的pid)
可以通过 -XX:NativeMemoryTracking=xx 来设置,选项有上面三种。
openjdk-jdk8u/jdk/src/share/bin/java.c.
jdk/src/share/bin/java.c
/*
* static void SetJvmEnvironment(int argc, char **argv);
* Is called just before the JVM is loaded.
* We can set env variables that are consumed by the JVM.
* This function is non-destructive, leaving the arg list intact.
* The first use is for the JVM flag -XX:NativeMemoryTracking=value.
*/
static void SetJvmEnvironment(int argc, char **argv) {
static const char* NMT_Env_Name = "NMT_LEVEL_";
int i;
for (i = 0; i < argc; i++) {
char *arg = argv[i];
if (i > 0) {
char *prev = argv[i - 1];
// 跳过一些jvm不需要的参数 / 非-开头的, / 如 -version or -help等 / 一些应用级参数
if (*arg != '-' && ((JLI_StrCmp(prev, "-cp") == 0
|| JLI_StrCmp(prev, "-classpath") == 0))) {
continue;
}
if (*arg != '-'
|| JLI_StrCmp(arg, "-version") == 0
|| JLI_StrCmp(arg, "-fullversion") == 0
|| JLI_StrCmp(arg, "-help") == 0
|| JLI_StrCmp(arg, "-?") == 0
|| JLI_StrCmp(arg, "-jar") == 0
|| JLI_StrCmp(arg, "-X") == 0) {
return;
}
}
// NativeMemoryTracking 参数是记录jvm向系统申请内存时的埋点,有一定的性能消耗,使用需要谨慎
if (JLI_StrCCmp(arg, "-XX:NativeMemoryTracking=") == 0) {
int retval;
// get what follows this parameter, include "="
size_t pnlen = JLI_StrLen("-XX:NativeMemoryTracking=");
if (JLI_StrLen(arg) > pnlen) {
char* value = arg + pnlen;
size_t pbuflen = pnlen + JLI_StrLen(value) + 10; // 10 max pid digits
/*
* ensures that malloc successful
* DONT JLI_MemFree() pbuf. JLI_PutEnv() uses system call
that could store the address.
*/
char * pbuf = (char*)JLI_MemAlloc(pbuflen);
JLI_Snprintf(pbuf, pbuflen, "%s%d=%s", NMT_Env_Name, JLI_GetPid(), value);
retval = JLI_PutEnv(pbuf);
if (JLI_IsTraceLauncher()) {
char* envName;
char* envBuf;
// ensures that malloc successful
envName = (char*)JLI_MemAlloc(pbuflen);
JLI_Snprintf(envName, pbuflen, "%s%d", NMT_Env_Name, JLI_GetPid());
printf("TRACER_MARKER: NativeMemoryTracking: env var is %s\n",envName);
printf("TRACER_MARKER: NativeMemoryTracking: putenv arg %s\n",pbuf);
envBuf = getenv(envName);
printf("TRACER_MARKER: NativeMemoryTracking: got value %s\n",envBuf);
free(envName);
}
}
}
}
}
3. LoadJavaVM()
加载libjvm.so 或 jvm.dll,并拿到其中部分函数地址后续使用。
openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c
jdk/src/solaris/bin/java_md_solinux.c
jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
void *libjvm;
JLI_TraceLauncher("JVM path is %s\n", jvmpath);
/*
通过dlopen函数打开jvm的动态库libjvm.so,并把它装入内存中,为了后面做符号解析和重定位
RTLD_NOW:在dlopen返回前,需要解析出所有未定义符号,否则,在dlopen会返回NULL
RTLD_GLOBAL:动态库中定义的符号可被其后打开的其它库重定位
*/
libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
if (libjvm == NULL) {
#if defined(__solaris__) && defined(__sparc) && !defined(_LP64) /* i.e. 32-bit sparc */
.............
#endif
JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
// dlsym函数对动态库libjvm.so做符号解析,如果库中有函数 JNI_CreateJavaVM 的符号,则把对应符号的函数地址赋值给 ifn->CreateJavaVM
ifn->CreateJavaVM = (CreateJavaVM_t)
dlsym(libjvm, "JNI_CreateJavaVM");
if (ifn->CreateJavaVM == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
// dlsym函数对动态库libjvm.so做符号解析,如果库中有函数 JNI_GetDefaultJavaVMInitArgs 的符号,则把对应符号的函数地址赋值给 ifn->GetDefaultJavaVMInitArgs
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
if (ifn->GetDefaultJavaVMInitArgs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
// dlsym函数对动态库libjvm.so做符号解析,如果库中有函数 JNI_GetCreatedJavaVMs 的符号,则把对应符号的函数地址赋值给 ifn->GetCreatedJavaVMs
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs");
if (ifn->GetCreatedJavaVMs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
return JNI_TRUE;
}
4. SetClassPath(cpath) / SetClassPath(what)
设置目标class文件路径 或 设置目标jar路径
5. JVMInit()
openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c
VM初始化入口
1、调用ShowSplashScreen函数展示Java虚拟机的欢迎画面
2、调用ContinueInNewThread函数创建一个新线程并在其中启动Java虚拟机,执行后续流程
jdk/src/solaris/bin/java_md_solinux.c
int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
// 调用ShowSplashScreen函数展示Java虚拟机的欢迎画面
ShowSplashScreen();
// 创建一个新线程并在其中启动Java虚拟机,执行后续流程
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
ContinueInNewThread()
main线程创建、执行
threadStackSize参数表示线程执行时的栈空间,因为每个线程执行时都要有自己的私有栈空间做数据存储,所以这是必须的, 这个值可以自己设置,不设置的话,系统会自己默认给个值:
linux64位系统默认是1024k,32位系统默认是320k(见下方截图), 另外,自己查看threadStackSize栈大小可以通过下列方式:
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
jdk/src/share/bin/java.c
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
/*
* 如果用户没有指定 threadStackSize 大小,那就要去检查下VM自带的默认值。
Hotspot本身不再支持jdk1.1版本,但可以通过init args结构返回其默认堆栈大小,
调用 GetDefaultJavaVMInitArgs(对应的实际函数是JNI_GetDefaultJavaVMInitArgs),
这个函数可以取到VM设置的默认的 threadStackSize 大小。
这里为什么用不支持jdk1.1但是又用jdk1.1来做参数,我想原因是历史遗留,当然这个也不重要,我们只需要知道这里是可以拿到默认 threadStackSize 大小的就行。
可通过如下命令查看
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
*/
if (threadStackSize == 0) {
struct JDK1_1InitArgs args1_1;
memset((void*)&args1_1, 0, sizeof(args1_1));
args1_1.version = JNI_VERSION_1_1;
ifn->GetDefaultJavaVMInitArgs(&args1_1); /* ignore return value */
if (args1_1.javaStackSize > 0) {
threadStackSize = args1_1.javaStackSize;
}
}
/* 创建一个新的线程去构建JVM, 新线程的入口函数是JavaMain() */
{
JavaMainArgs args;
int rslt;
args.argc = argc;
args.argv = argv;
args.mode = mode;
args.what = what;
args.ifn = *ifn;
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
// 如果调用者认为存在错误(ret就是调用者给过来的),我们只需返回错误,否则我们返回被调用者的值
return (ret != 0) ? ret : rslt;
}
}
hotspot/src/os_cpu/linux_x86/vm/globals_linux_x86.hpp
ContinueInNewThread0()
这个函数就是通过系统调用API创建一个新的线程,线程入口是 参数continuation指向的函数指针指向的函数(即JavaMain())。
/*
* Block current thread and continue execution in a new thread
*/
jdk/src/solaris/bin/java_md_solinux.c
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
int rslt;
#ifndef __solaris__
pthread_t tid; // 线程ID
pthread_attr_t attr; // 线程属性
pthread_attr_init(&attr);// 初始化线程属性结构
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size); // 设置栈大小
}
/* 设置线程的分离状态
* detachstate:
* PTHREAD_CREATE_DETACHED 表示分离
* PTHREAD_CREATE_JOINABLE 表示结合
* 如果设置成分离状态,表示无需关注新创建的线程执行结果,由它自行完成,并在完成后由操作系统回收资源;
* 如果设置成非分离状态,表示父线程需要拿到创建的子线程的执行结果
这里是Java进程,要等待Main线程的执行结果
*/
// 创建一个新的线程,指定线程创建后需要执行的任务函数,返回0表示创建成功。
if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
void * tmp;
pthread_join(tid, &tmp); // 等待子线程执行结果
rslt = (int)tmp;
} else {
// 线程创建失败,就在当前线程下执行任务函数
rslt = continuation(args);
}
pthread_attr_destroy(&attr);
#else /* __solaris__ */
..............
#endif /* !__solaris__ */
return rslt;
}
3. JavaMain() main线程在这里
在上一步创建main线程后, 其执行的入口就是JavaMain();
它主要做三件事:
1. 创建vm;
2. 加载主类;
3. 执行主类的main方法。
jdk/src/share/bin/java.c
int JNICALL
JavaMain(void * _args)
{
// 参数赋值
JavaMainArgs *args = (JavaMainArgs *)_args;
int argc = args->argc;
char **argv = args->argv;
int mode = args->mode; // 类加载 还是jar包加载
char *what = args->what; // 类路径 或 jar包路径
InvocationFunctions ifn = args->ifn;
JavaVM *vm = 0;
JNIEnv *env = 0;
jclass mainClass = NULL;
jclass appClass = NULL; // actual application class being launched
jmethodID mainID;
jobjectArray mainArgs;
int ret = 0;
jlong start = 0, end = 0;
// 这个函数在linux和windows实现中都是空白,把它看作java里的一个钩子函数
RegisterThread();
start = CounterGet();
---1. 初始化虚拟机
if (!InitializeJVM(&vm, &env, &ifn)) {
// 初始化失败,打印日志,退出程序
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
// 是否打印设置过的信息
if (showSettings != NULL) {
ShowSettings(env, showSettings);
CHECK_EXCEPTION_LEAVE(1);
}
// 打印版本信息
if (printVersion || showVersion) {
PrintJavaVersion(env, showVersion);
CHECK_EXCEPTION_LEAVE(0);
if (printVersion) {
LEAVE();
}
}
/* If the user specified neither a class name nor a JAR file */
if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
PrintUsage(env, printXUsage);
CHECK_EXCEPTION_LEAVE(1);
LEAVE();
}
// 释放启动虚拟机时加载的jvm.cfg中配置项占用的空间,见之前章节CreateExecutionEnvironment, 略过...
FreeKnownVMs(); /* after last possible PrintUsage() */
if (JLI_IsTraceLauncher()) {
end = CounterGet();
JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
(long)(jint)Counter2Micros(end-start));
}
/* At this stage, argc/argv have the application's arguments */
if (JLI_IsTraceLauncher()){
int i;
printf("%s is '%s'\n", launchModeNames[mode], what);
printf("App's argc is %d\n", argc);
for (i=0; i < argc; i++) {
printf(" argv[%2d] = '%s'\n", i, argv[i]);
}
}
ret = 1;
---2. 加载执行主入口类main class
mainClass = LoadMainClass(env, mode, what);
CHECK_EXCEPTION_NULL_LEAVE(mainClass);
// LoadMainClass函数求出了mainClass,其实appClass跟mainClass是相同的,所以,这一步拿得就是mainClass,后面会讲这个细节
appClass = GetApplicationClass(env);
NULL_CHECK_RETURN_VALUE(appClass, -1);
// 看成钩子函数就行,linux/windows都没做具体实现
PostJVMInit(env, appClass, vm);
CHECK_EXCEPTION_LEAVE(1);
// 取出主类mainClass的main函数地址,并赋值给mainID
mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");
CHECK_EXCEPTION_NULL_LEAVE(mainID);
/* 构建平台依赖的参数数组 */
mainArgs = CreateApplicationArgs(env, argv, argc);
CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
---3. 这里才真正调用 java类中的main方法.
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
/*
* 如果exit code不为0,那么main执行抛出了异常
* 至此,整个java应用执行完毕
*/
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
LEAVE(); // 这是一个宏定义的函数,内部主要做一次资源回收的工作
}
InitializeJVM()
JVM初始化
/*
* Initializes the Java Virtual Machine. Also frees options array when finished.
* 初始化java 虚拟机,完成后释放options数组,option就是虚拟机启动时设置的参数,
*/
jdk/src/share/bin/java.c
static jboolean InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{
JavaVMInitArgs args;
jint r;
// args空间填0
memset(&args, 0, sizeof(args));
args.version = JNI_VERSION_1_2; // 版本
args.nOptions = numOptions;
// option数量
args.options = options;
// options数组
args.ignoreUnrecognized = JNI_FALSE;
if (JLI_IsTraceLauncher()) {
int i = 0;
printf("JavaVM args:\n ");
printf("version 0x%08lx, ", (long)args.version);
printf("ignoreUnrecognized is %s, ",
args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
printf("nOptions is %ld\n", (long)args.nOptions);
for (i = 0; i < numOptions; i++)
printf(" option[%2d] = '%s'\n",
i, args.options[i].optionString);
}
// ifn->CreateJavaVM函数,实际指向的是加载的动态库libjvm.so 中的 JNI_CreateJavaVM函数,JNI_开头的函数,都定义在jni.cpp文件中,
// 通过CreateJavaVM函数真正创建Java虚拟机
r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
// 初始化结束, 释放options
JLI_MemFree(options);
return r == JNI_OK;
}
1. JNI_CreateJavaVM() 创建vm
_JNI_IMPORT_OR_EXPORT_ __attribute__((visibility("default"))) 用于gcc编译动态链接库时,向外暴露函数符号。
openjdk-jdk8u/hotspot/src/share/vm/prims/jni.cpp
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
#ifndef USDT2
HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args);
#else /* USDT2 */
HOTSPOT_JNI_CREATEJAVAVM_ENTRY((void **) vm, penv, args);
#endif /* USDT2 */
jint result = JNI_ERR;
DT_RETURN_MARK(CreateJavaVM, jint, (const jint&)result); // 日志记录用
// 这一段是检验Atomic::xchg(汇编命令:原子性更改变量的值)是否可用,
// jvm用Atomic::xchg来实现synchronization同步。
#if defined(ZERO) && defined(ASSERT)
{
jint a = 0xcafebabe;
jint b = Atomic::xchg(0xdeadbeef, &a);
void *c = &a;
void *d = Atomic::xchg_ptr(&b, &c);
assert(a == (jint) 0xdeadbeef && b == (jint) 0xcafebabe, "Atomic::xchg() works");
assert(c == &b && d == &a, "Atomic::xchg_ptr() works");
}
#endif // ZERO && ASSERT
// 原子操作:设置vm_created为1,防止其他线程创建jvm
if (Atomic::xchg(1, &vm_created) == 1) {
return JNI_EEXIST; // 已经创建,或者在创建过程中
}
if (Atomic::xchg(0, &safe_to_recreate_vm) == 0) {
return JNI_ERR; // 尝试失败后就不允许重试
}
assert(vm_created == 1, "vm_created is true during the creation");
bool can_try_again = true;
result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
if (result == JNI_OK) {
JavaThread *thread = JavaThread::current(); // 当前线程,其实就是主线程
*vm = (JavaVM *)(&main_vm); // 将vm指针指向create_vm中创建的vm,后续操作要用。
*(JNIEnv**)penv = thread->jni_environment(); // 同样的pJNIEnv指针指向create_vm中在JavaThread中创建的JNIEnv。
RuntimeService::record_application_start();
// 通知 JVMTI(jvm的黑匣子,记录jvm的运行情况,可以通过调用jvmti的接口对jvm进行管理)
if (JvmtiExport::should_post_thread_life()) {
JvmtiExport::post_thread_start(thread);
}
post_thread_start_event(thread);
...
// Since this is not a JVM_ENTRY we have to set the thread state manually before leaving.
ThreadStateTransition::transition_and_fence(thread, _thread_in_vm, _thread_in_native);
} else { // create_vm 创建失败
if (can_try_again) {
// 可重试就把safe_to_recreate_vm设置为1
safe_to_recreate_vm = 1;
}
// 创建失败了,要重置vm_created
// Creation failed. We must reset vm_created
*vm = 0;
*(JNIEnv**)penv = 0;
// 释放创建过程vm_created空间
OrderAccess::release_store(&vm_created, 0);
}
return result;
}
Threads::create_vm((JavaVMInitArgs*) args, &can_try_again)
这是个大块头,以后的章节我们会以它为入口,串联起VM的相关知识,现在先略过它。
2. LoadMainClass() 加载main类
加载main class
/*
* 加载一个类,并验证主类是否存在,
JNIEnv *env: 指向 JNI 环境的指针。
int mode: 模式参数,用于加载类的方式。
char *name: 要加载的类名 或 jar包路径
*/
static jclass LoadMainClass(JNIEnv *env, int mode, char *name) {
jmethodID mid; // 方法ID
jstring str; // 字符串对象
jobject result; // 结果对象
jlong start = 0, end = 0; // 计时器
jclass cls = GetLauncherHelperClass(env); // 获取启动器帮助类 sun/launcher/LauncherHelper.java
NULL_CHECK0(cls); // 检查类是否为空
if (JLI_IsTraceLauncher()) {
start = CounterGet(); // 开始计时
}
// 获取LauncherHelper类 静态方法 checkAndLoadMain() ID
NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
"checkAndLoadMain",
"(ZILjava/lang/String;)Ljava/lang/Class;"));
// 将 C 字符串转换为 Java 字符串
str = NewPlatformString(env, name);
// 调用Java类LauncherHelper#checkAndLoadMain() 加载主类,支持类名 和 jar包两种加载方式
// 这里它利用 应用类加载器(Application ClassLoade),(通过ClassLoader.getSystemClassLoader()得到)
// 来加载目标类, 如果对类加载器的知识模糊了, 后续章节我们会详细介绍
CHECK_JNI_RETURN_0(
result = (*env)->CallStaticObjectMethod(
env, cls, mid, USE_STDERR, mode, str));
if (JLI_IsTraceLauncher()) {
end = CounterGet(); // 结束计时
printf("%ld micro seconds to load main class\n",
(long)(jint)Counter2Micros(end-start)); // 打印加载主类所用时间
printf("----%s----\n", JLDEBUG_ENV_ENTRY); // 打印调试信息
}
return (jclass)result; // 返回加载的类
}
3. CallStaticVoidMethod() 执行main方法。
真正去执行java类中的main函数
加载到了目标类,定位到了其main()函数,接下来就是调用了。
函数名CallStaticVoidMethodV中的static 已经指示出了它专用来调用静态方法的。
void CallStaticVoidMethod(jclass cls, jmethodID methodID, ...) {
va_list args;
va_start(args,methodID);
functions->CallStaticVoidMethodV(this,cls,methodID,args);
va_end(args);
}
总结
到此呢,从执行java命令 到 vm 执行java类的main()方法 的全过程就展现出来了。
当然了,这只是冰山一角,九牛中的一毛而已,但是我们已经初窥全貌了,对整体流程有了一定了解了。
初次探索总是略显痛苦,不用气馁,不用害怕,在后续的文章里我们会一步一步来探索更多的细节。
标签:java,int,流程,args,char,全貌,JNI,main,JLI From: https://blog.csdn.net/capaabbcc/article/details/142426755