BruceBat's Blog
BruceBat's Blog

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生


  • 首页

  • 归档

  • 分类

  • 标签

  • 搜索
NIO I/O 计算机科学 操作系统 设计模式 随记 WebSocket 计算机网络 注册中心 经典电影 xxl-job 分布式 分布式任务调度 MySQL DevOps Docker 多线程 有趣的问题 Mybatis-Plus Mybatis Java 数据结构

探秘Java:从main函数启动开始

发表于 2021-06-23 | 分类于 Java | 0 | 阅读次数 1302
从main函数开始
从main函数开始
“

人生苦短,不如养狗

”

一、前言

  不知道在座的各位朋友是否跟我一样,初学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 from\n%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 wechat
一个闲鱼程序猿的微信公众号
  • 本文作者: brucebat
  • 本文链接: https://www.swzgeek.com/archives/2021-06-13
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# NIO # I/O # 计算机科学 # 操作系统 # 设计模式 # 随记 # WebSocket # 计算机网络 # 注册中心 # 经典电影 # xxl-job # 分布式 # 分布式任务调度 # MySQL # DevOps # Docker # 多线程 # 有趣的问题 # Mybatis-Plus # Mybatis # Java # 数据结构
Java中的设计模式(三):装饰器模式
Java中的设计模式(四):代理模式
  • 文章目录
  • 站点概览
brucebat

brucebat

一个有梦想的咸鱼程序猿

46 日志
8 分类
22 标签
RSS
Github E-mail
Creative Commons
© 2020 — 2023 brucebat
苏ICP备20002207号-1

苏公网安备 32011302320859号

0%