JVM执行程序时,都发生了什么?

JVM执行程序时,都发生了什么?

当一个Java程序被执行时,JVM都做了什么?今天这篇文章,记录了我的探索过程。让我们开始吧!

下文使用的程序选自 Log4j文档中的示例程序。
JDK 版本 :
java version "12" 2019-03-19
Java(TM) SE Runtime Environment (build 12+33)

Java HotSpot(TM) 64-Bit Server VM (build 12+33, mixed mode, sharing)

package self.samson.bu;

// Import log4j classes.
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class MyApp {

    // Define a static logger variable so that it references the
    // Logger instance named "MyApp".
    private static final Logger logger = LogManager.getLogger(MyApp.class);

    public static void main(final String... args) {

        // Set up a simple configuration that logs on the console.

        logger.trace("Entering application.");
        Bar bar = new Bar();
        if (!bar.doIt()) {
            logger.error("Didn't do it.");
        }
        logger.trace("Exiting application.");
    }
}

package self.samson.bu;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class Bar {
    static final Logger logger = LogManager.getLogger(Bar.class.getName());

    public boolean doIt() {
        logger.entry();
        logger.error("Did it again!");
        return logger.exit(false);
    }
}

JVM执行过程

首先,先看一下Java Language Specification, jls中描述的JVM执行过程。

  1. 加载主类
  2. 链接:验证、预处理和解析(可选地)
  3. 初始化:执行初始化器
  4. 执行主方法

接下来,结合jls中定义步骤,分析一下JVM执行MyAppmain方法的过程。

加载主类self.samson.bu.MyApp

当JVM首次尝试执行MyApp类的main方法时,会发现该类尚未被加载到内存中,即内存中不存在与MyApp.class相对应的Class对象。此时,JVM会使用类加载器,将MyApp.class加载到内存中。调试发现,此时使用的类加载器为AppClassLoader。如果类加载器无法在classpath中找到MyApp.class,JVM将抛出NoClassDefFoundError

链接

主类被加载过后,JVM会对加载的类进行链接。链接过程主要包括验证、预处理和解析。

  1. 验证。首先,JVM会验证加载的MyApp类是否是格式良好的、是否具有适当的符号表、以及是否遵循Java编程语言和JVM语义规范。
  2. 预处理。其次,JVM会为类中的静态域(类变量和常量)分配空间,并把这些空间初始化为默认值。注意,此时不会执行任何源代码,初始化器的执行会在初始化阶段,而不是预处理阶段。除了为MyApp中的静态变量(logger)分配空间外,JVM还需要为一些它内部使用的对象(例如方法表等)分配空间。
  3. 解析。最后,检查MyApp对其他类或接口的引用,加载这些类或接口,并检查这些引用是否正确。这一步是可选地,如果没有引用其他类,此阶段可以跳过。这里需要加载org.apache.logging.log4j.Logger

初始化

初始化阶段会执行类变量初始化器(initializer)和静态初始化器。在MyApp中,只有一个类变量初始化器,即 logger = LogManager.getLogger(MyApp.class);。当JVM尝试调用org.apache.logging.log4j.LogManager类的getLogger方法时,它会发现该类尚未加载。JVM会按照之前的进程去加载、链接、初始化LogManager类。LoggerManager包含多个类变量初始化器和一个静态初始化器。如下所示:

// 类变量初始化器
public static final String FACTORY_PROPERTY_NAME = "log4j2.loggerContextFactory";
public static final String ROOT_LOGGER_NAME = "";
private static final Logger LOGGER = StatusLogger.getLogger();
private static final String FQCN = LogManager.class.getName();
private static volatile LoggerContextFactory factory;
// 静态初始化器
static {
    PropertiesUtil managerProps = PropertiesUtil.getProperties();
    String factoryClassName = managerProps.getStringProperty("log4j2.loggerContextFactory");
    if (factoryClassName != null) {
        try {
            factory = (LoggerContextFactory)LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
        } catch (ClassNotFoundException var8) {
            LOGGER.error("Unable to locate configured LoggerContextFactory {}", factoryClassName);
        } catch (Exception var9) {
            LOGGER.error("Unable to create configured LoggerContextFactory {}", factoryClassName, var9);
        }
    }
    // more...        
}

这些初始化器的执行,会按照文本顺序进行。执行过程还会引起JVM对其他类(例如org.apache.logging.log4j.status.StatusLoggerorg.apache.logging.log4j.util.PropertiesUtil等)的加载。过程与上面介绍的MyApp类的加载过程类似,此处就不再过多地叙述。

等所有的依赖都加载并初始化完毕后,MyApp类的初始化才会结束。此外,MyApp的初始化必须在它的直接父类或父接口的初始化之后完成。前面的代码中,MyApp的直接父类只有java.lang.Object,如果它还没被初始化过,会先初始化Object类。

类的初始化过程包括:执行静态初始化器和执行静态变量(类变量)的初始化器。接口的初始化过程包括:执行域(常量)的初始化器。jls中定义了类或接口T初始化发生的时机:

  • T为类时,创建T的对象时,会触发T的初始化;
  • T中定义的静态方法被调用时,会触发T的初始化;
  • T中定义的静态域被赋值时,会触发T的初始化;
  • T中定义的非常量域被使用时,会触发T的初始化。
Initialization of an interface does not, of itself, cause initialization of any of its superinterfaces.
接口的初始化本身不会导致任何超接口的初始化。

执行主方法

MyApp的初始化过程结束后,JVM会再次尝试执行它的main方法。jls中定义了两种可接受的main方法声明方式:

  • public static void main(String[] args)
  • public static void main(String... args)

你可能感兴趣的:(java,jvm)