“
人生苦短,不如养狗
”
一、前言
不知道在座的各位朋友是否跟我一样,初学Java时写下的第一段代码就是类似下面这段代码:
public static void main(String[] args) {
System.out.println("hello, world!");
}
上面代码的功能非常简单,就是在控制台中打印出”hell, world!”这句话。当然今天我们要关注的不是这段代码实现的功能,而是这段代码出现的地方,也就是 main函数 。
二、万物始于main函数
回顾曾经写过的代码,无论是复杂的微服务项目,还是一行简单的 System.out.println()
,代码的入口函数一定是main函数,这已经成为编写代码时无需质疑的定式。但作为一个有梦想的程序猿,做事要知其然也要知其所以然,下面就让我们一起来探究一下为何万物始于main函数。
1. 为什么是main函数
众所周知,我们编写的Java文件都是运行在JVM虚拟机上面,也就是说程序的编译和运行都是要遵循JVM的规定,那么我们就来看一看JVM源码中是如何规定的。
在JVM启动程序中定义了这样一个方法 int JNICALL JavaMain(void * args);
,在这个方法中确定了如何加载Java应用程序的入口类和入口方法,这里我们暂时省略其他代码,直接阅读一下加载入口方法的代码:
/* 获取应用的main */
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
if (mainID == NULL) {
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
} else {
message = "No main method found in specified class.";
messageDest = JNI_TRUE;
}
goto leave;
}
{ /* 确保main方法是公有的 */
jint mods;
jmethodID mid;
jobject obj = (*env)->ToReflectedMethod(env, mainClass,
mainID, JNI_TRUE);
if( obj == NULL) { /* 抛出异常 */
ReportExceptionDescription(env);
goto leave;
}
mid =
(*env)->GetMethodID(env,
(*env)->GetObjectClass(env, obj),
"getModifiers", "()I");
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
goto leave;
}
mods = (*env)->CallIntMethod(env, obj, mid);
if ((mods & 1) == 0) { /* if (!Modifier.isPublic(mods)) ... */
message = "Main method not public.";
messageDest = JNI_TRUE;
goto leave;
}
}
/* Build argument array */
mainArgs = NewPlatformStringArray(env, argv, argc);
if (mainArgs == NULL) {
ReportExceptionDescription(env);
goto leave;
}
/* 执行main方法 */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
在上面的代码中我们可以看到,JVM在启动过程中会根据指定的 MainClass
即初始类去获取该类中的 main
方法,同时这里也明确了main方法必须是静态的、公有的且参数列表为 String
数组。看到这里,想必大家应该明白为什么在编写Java程序时入口函数一定需要是main函数了。
2. main函数如何执行
了解了为什么Java程序的入口方法一定是main方法,下面我们再来了解一下一个包含main方法的Java程序到底是如何被执行的。
当我们在idea中去执行上述代码时,实际上执行的是这样一行命令:
java {类名}.java
在上面这行命令中出现的 java
指令实际上是jdk提供的执行java程序的指令,指令后面紧跟着的文件名就是待执行的java程序。这行命令会启动 java.exec 这样一个可执行程序,在这个可执行程序中会执行 src/share/tools/launcher/java.c
文件中的main方法,进行JVM启动前的运行环境版本检查、配置初始化并创建一个JVM进程来执行Java程序,执行Java程序的过程就是上面代码展示的寻找并调用入口类的main方法。
需要注意的是JVM执行的java程序是已经编译完成的 .class文件 ,也即在执行指令之处会执行 javac
指令对.java文件进行编译,然后在进行执行上述的操作。这里从上面获取java程序中main方法的代码中也可以看出来:
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
这里参数列表的写法就是编译后的二进制.class文件中的写法,有兴趣的同学可以通过idea自带的查看二进制文件的工具自行查看一下。
接下来的逻辑就是我们日常了解到的JVM加载、链接和初始化类和接口的流程,这里就不做过多的展开。
3. Java程序的执行方式
在日常的开发过程中,除了上面直接运行一个java文件,我们大部分情况都是将Java程序打包成一个jar包进行运行,这里从源码中也能得窥一二。
if (jarfile != 0) {
// 如果使用jar方式运行,则从对应jar中获取mainClass
mainClassName = GetMainClassName(env, jarfile);
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
goto leave;
}
if (mainClassName == NULL) {
const char * format = "Failed to load Main-Class manifest "
"attribute fromn%s";
message = (char*)JLI_MemAlloc((strlen(format) + strlen(jarfile)) *
sizeof(char));
sprintf(message, format, jarfile);
messageDest = JNI_TRUE;
goto leave;
}
classname = (char *)(*env)->GetStringUTFChars(env, mainClassName, 0);
if (classname == NULL) {
ReportExceptionDescription(env);
goto leave;
}
mainClass = LoadClass(env, classname);
if(mainClass == NULL) { /* exception occured */
const char * format = "Could not find the main class: %s. Program will exit.";
ReportExceptionDescription(env);
message = (char *)JLI_MemAlloc((strlen(format) +
strlen(classname)) * sizeof(char) );
messageDest = JNI_TRUE;
sprintf(message, format, classname);
goto leave;
}
(*env)->ReleaseStringUTFChars(env, mainClassName, classname);
} else {
// 直接指定入口mainClass的className
mainClassName = NewPlatformString(env, classname);
if (mainClassName == NULL) {
const char * format = "Failed to load Main Class: %s";
message = (char *)JLI_MemAlloc((strlen(format) + strlen(classname)) *
sizeof(char) );
sprintf(message, format, classname);
messageDest = JNI_TRUE;
goto leave;
}
classname = (char *)(*env)->GetStringUTFChars(env, mainClassName, 0);
if (classname == NULL) {
ReportExceptionDescription(env);
goto leave;
}
mainClass = LoadClass(env, classname);
if(mainClass == NULL) { /* exception occured */
const char * format = "Could not find the main class: %s. Program will exit.";
ReportExceptionDescription(env);
message = (char *)JLI_MemAlloc((strlen(format) +
strlen(classname)) * sizeof(char) );
messageDest = JNI_TRUE;
sprintf(message, format, classname);
goto leave;
}
(*env)->ReleaseStringUTFChars(env, mainClassName, classname);
}
从上述的代码中可以看到,获取应用程序的主类分为两种情况,一种是通过获取jar包中 META-INF/MANIFEST.MF
文件中指定的Main-Class类名,一种是通过指定className来获取(也就是上面示例中方式)。
三、总结
心血来潮重新回顾了一下当初只知其然不知其所以然的知识点,分析不一定到位,仅供大家参考。上面分析使用的jdk源码是基于openJDK官网提供的 hotspot 1.7版本的源码,有兴趣的同学可以到官网上面下载源码阅读学习一下。
原文始发于微信公众号(Brucebat的伪技术鱼塘):探秘Java:从main函数启动开始
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/90115.html