当一个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); } }
首先,先看一下 Java Language Specification, jls 中描述的JVM执行过程。
接下来,结合jls中定义步骤,分析一下JVM执行 MyApp
中 main
方法的过程。
self.samson.bu.MyApp
当JVM首次尝试执行 MyApp
类的 main
方法时,会发现该类尚未被加载到内存中,即内存中不存在与 MyApp.class
相对应的 Class
对象。此时,JVM会使用类加载器,将 MyApp.class
加载到内存中。调试发现,此时使用的类加载器为 AppClassLoader
。如果类加载器无法在 classpath
中找到 MyApp.class
,JVM将抛出 NoClassDefFoundError
。
主类被加载过后,JVM会对加载的类进行链接。链接过程主要包括验证、预处理和解析。
MyApp
类是否是格式良好的、是否具有适当的符号表、以及是否遵循Java编程语言和JVM语义规范。 MyApp
中的静态变量( logger
)分配空间外,JVM还需要为一些它内部使用的对象(例如方法表等)分配空间。 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.StatusLogger
、 org.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)