天气不错,闲来无事,所以搞点事情吧...
作为一名 JAVA
开发者,不知道大家有没有去想过,JAVA
程序为什么一定要从 main 函数执行开始,其实关于这个话题,我大概从网上搜了下,其实不乏有 main 方法是我们学习Java语言学习的第一个方法,也是每个 java 使用者最熟悉的方法, 每个 Java 应用程序都必须有且仅有一个 main 方法 这种说法。那么真的是这样吗?今天就来聊聊这个事情。
为什么 main 函数是 java 执行入口
我们在通过一般的 IDE 去 debug 时,main 函数确实是在堆栈的最开始地方...
但是如果你熟悉 SpringBoot 的启动过程,你会知道,你看到的 main 函数并不是真正开始执行启动的 main 函数,关于这点,我在之前 SpringBoot 系列-FatJar 启动原理 这篇文章中有过说明;即使是通过 JarLaunch 启动,但是入口还是 main,只不过套了一层,然后反射去调用你应用的 main 方法。
public class JarLauncher extends ExecutableArchiveLauncher {
// BOOT-INF/classes/
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
// BOOT-INF/lib/
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
// 空构造函数
public JarLauncher() {
}
// 省略无关代码...
// main 函数
public static void main(String[] args) throws Exception {
// 引导启动入口
new JarLauncher().launch(args);
}
}
复制代码
这里抛开 JarLaunch 这种引导启动的方式,单从最普通 java 程序来看,我们来看下 main 函数作为入口的原因。
找的最开始、最遥远的地方
JDK 里面的代码太多了,如果在不清楚的情况下去找,那和大海捞针差不多;那我们想一下,既然 java 要去执行 main,首先它要找到这个 main,那 main 方法是写在我们代码里面的,所以对于 java 来说,它就不得不去先把我们包含 main 方法的类加载起来。所以:
我们找到了 LauncherHelper#checkAndLoadMain
这个上一层入口;通过这个方法的代码注释,我们就知道了,网上关于介绍 main 作为启动方法的一系列验证是缘起何处了:
- 可以从 fatjar manifest 中找到启动类的 classname
- 使用 System ClassLoader 加载这个类
验证这个启动类的合法性
- 这个类是否存在
- 有没有 main 函数
- 是不是 static 的
- 是不是 public 的
- 有没有 string 数组作为参数
- 如果没有 main 函数,那当前的这个类是不是继承了 FX Application(关键)
PS: 这里摘取一篇关于为什么是 public 的描述:JAVA 指定了一些可访问的修饰符如:private,protected,public。每个修饰符都有它对应的权限,public 权限最大,为了说明问题,我们假设 main 方法是用 private 修饰的,那么 main 方法出了 Demo 这个类对外是不可见的。那么,JVM 就访问不到 main 方法了。因此,为了保证JVM在任何情况下都可以访问到main方法,就用 public修饰
这个说法我个人理解是有点欠妥的,首先是 java 里面有反射机制,访问修饰符的存在在 JVM 规范里面说的最多的是因为安全问题,并不是 JVM 能不能访问的问题,因为 JVM 里面有一百种方式去访问一个 private。
LauncherHelper 被执行调用的地方
从堆栈看,checkAndLoadMain 上层没有了,那猜测可能就是有底层 JVM(c 部分)来驱动的。继续去扒一下,在 jdk 的 java.c 文件中捞到了如下代码片段:
jclass GetLauncherHelperClass(JNIEnv *env) {
if (helperClass == NULL) {
NULL_CHECK0(helperClass = FindBootStrapClass(env,
"sun/launcher/LauncherHelper"));
}
return helperClass;
}
复制代码
到这也论证了前面的猜测,确实是由底层来驱动执行的。那么既然都看到这里了,也有必要看下我们的 JAVA 程序启动、JVM 启动过程是怎样的。
JVM 是如何驱动 JAVA 程序执行的
这里我的思路还是从可以见的代码及堆栈一层一层往上去拨的,通过 GetLauncherHelperClass 找到了 LoadMainClass,后面再找打整体启动入口。
LoadMainClass
下面是代码(代码的可读性和理解要比文字更直接):
/*
* Loads a class and verifies that the main class is present and it is ok to
* call it for more details refer to the java implementation.
*/
static jclass LoadMainClass(JNIEnv *env, int mode, char *name) {
jmethodID mid;
jstring str;
jobject result;
jlong start, end;
// 去找到 LauncherHelper
jclass cls = GetLauncherHelperClass(env);
NULL_CHECK0(cls);
// 根据 _JAVA_LAUNCHER_DEBUG 环境变量决策是否设置来打印 debug 信息
if (JLI_IsTraceLauncher()) {
start = CounterGet();
}
// 这里可以看到就是调用 LauncherHelper#checkAndLoadMain 的入口
NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
"checkAndLoadMain",
"(ZILjava/lang/String;)Ljava/lang/Class;"));
// 创建类名的 String 对象,也就是我们的启动类名
str = NewPlatformString(env, name);
// 调用静态对象方法 -> main
result = (*env)->CallStaticObjectMethod(env, cls, mid, USE_STDERR, mode, str);
if (JLI_IsTraceLauncher()) {
end = CounterGet();
printf("%ld micro seconds to load main classn",
(long)(jint)Counter2Micros(end-start));
printf("----%s----n", JLDEBUG_ENV_ENTRY);
}
return (jclass)result;
}
复制代码
Java 程序的 Entry point
对于 C/C++ 来说,其启动入口和 java 一样,也都是 main。下面我们略过一些无关代码,将 JAVA 程序驱动启动的核心流程代码梳理下
1、入口,main.c 的 main 方法 -> JLI_Launch
int
main(int argc, char **argv) {
// 省略其他代码 ...
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), const_jargs,
sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
FULL_VERSION,
DOT_VERSION,
(const_progname != NULL) ? const_progname : *margv,
(const_launcher != NULL) ? const_launcher : *margv,
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
const_cpwildcard, const_javaw, const_ergo_class);
}
复制代码
2、JLI_Launch,JVM 的实际 Entry point
/*
* Entry point.
*/
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 */
const char* lname, /* launcher name */
jboolean javaargs, /* JAVA_ARGS */
jboolean cpwildcard, /* classpath wildcard*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy */
) {
// 省略无关代码
// main class
char *main_class = NULL;
// jvm 路径
char jvmpath[MAXPATHLEN];
// jre 路径
char jrepath[MAXPATHLEN];
// jvm 配置路径
char jvmcfg[MAXPATHLEN];
// 省略无关代码 ...
// 选择运行时 jre 的版本,会有一些规则
SelectVersion(argc, argv, &main_class);
// 创建执行环境,包括找到 JRE、确定 JVM 类型、初始化 jvmpath 等等
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));
// 省略无关代码 ...
// 从 jvmpath load 一个 jvm
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
// 设置 classpath
// 解析参数,如 -classpath、-jar、-version、-verbose:gc .....
if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
{
return(ret);
}
/* java -jar 启动的话,要覆盖 class path */
if (mode == LM_JAR) {
SetClassPath(what);
}
// 省略无关代码 ...
//
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
复制代码
JVMInit
JVMInit 对于不同的操作系统有不同的实现,这里以 linux 的实现为例:
int JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret) {
ShowSplashScreen();
// 新线程的入口函数进行执行,新线程创建失败就在原来的线程继续支持这个函数
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
复制代码
这里比较深,ContinueInNewThread 里面又使用了一个 ContinueInNewThread0,从代码解释来看,大概意思是:先把当前线程阻塞,然后使用一个新的线程去执行,如果新线程创建失败就在原来的线程继续支持这个函数。核心代码:
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
复制代码
JavaMain
1、这里第一个比较关键的就是 InitializeJVM,初始化创建一个 Java Virtual Machine(jvm.so -> CreateJavaVM 代码比较多,实际上真正的初始化和启动jvm,是由 jvm.so 中的JNI_CreateJavaVM 实现)。
2、接下来就是到我们前面反推到的 LoadMainClass 了,找到我们真正 java 程序的入口类,就是我们应用程序带有 main 函数的类。
3、获取应用程序 Class -> GetApplicationClass,这里简单说下,因为和最后的那个 demo 有关,也和本文的题目有关。
// 在某些情况下,当启动一个需要助手的应用程序时,
// 例如,一个没有主方法的 JavaFX 应用程序,mainClass将不是应用程序自己的主类,
// 而是一个助手类
appClass = GetApplicationClass(env);
复制代码
4、调用 main 函数执行应用进程启动
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
复制代码
简单回顾
对于平常我们常见的 java 应用程序来说,main 函数确实作为执行入口,这个是有底层 JVM 驱动执行逻辑决定。但是从整个分析过程也可以看出,main 函数并不是唯一一种入口,那就是以非 main 入口启动的方式,也就是 JavaFX。
使用 FX Application 方式启动 java 程序
JAVA GUI 的旅程开始于 AWT,后来被一个更好的 GUI 框架所取代,其被称为 Swing。Swing 在GUI 领域有将近 20 年的历史。但是,它缺乏许多当今需求的视觉功能,其不仅要求可在多个设备上运行,还要有很好的外观和感觉。在 JAVA GUI 领域最有前景的是JavaFX,JAVA 自带的三个 GUI 工具包--AWT,Swing,和 JavaFX -- 它们做几乎相同的工作。而 JavaFX 使用一些不同的方法进行 GUI 编程,本文不针对 JavaFX 展开细说,有兴趣的同学可以自行查阅。
每一个 JavaFX 应用程序是应用程序类的扩展,其提供了应用程序执行的入口点。一个独立的应用程序通常是通过调用这个类定义的静态方法来运行的。应用程序类定义了三个生命周期的方法:init(), start() 和 stop()。
那么结合上一节中关于启动入口的讨论,这里给出一个小 demo 来把一个 springboot 工程启动起来(基于 ide,java -jar 可能会有区别,这里未验证)
@SpringBootApplication
public class Log4j2GuidesApplication extends Application {
// main -> mains
public static void mains(String[] args) throws Exception {
SpringApplication.run(Log4j2GuidesApplication.class, args);
System.out.println("222");
}
@Override
public void start(Stage stage) throws Exception {
mains(new String[0]);
System.out.println("111");
}
}
复制代码
这里有一个有意思的情况,一般情况下,如果没有非守护线程存活(通常是 web 模块提供)时进程会在启动完之后就退出,但是这里我没有开启 web 端口,但是启动完时,进程并没有退出,即使在 start 里面抛出异常,也不能显示的去阻断,这和 JavaFX Application 的生命周期有关,前面有提到。
总结
JAVA 应用的启动不一定是非要是 main 作为入口,关于其他的引导启动方式没有继续调研,如果大家有知道其他方式,也欢迎留言补充。
参考:《2020最新Java基础精讲视频教程和学习路线!》
链接:https://juejin.cn/post/691863...