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执行过程。
- 加载主类
- 链接:验证、预处理和解析(可选地)
- 初始化:执行初始化器
- 执行主方法
接下来,结合jls中定义步骤,分析一下JVM执行MyApp
中main
方法的过程。
加载主类self.samson.bu.MyApp
当JVM首次尝试执行MyApp
类的main
方法时,会发现该类尚未被加载到内存中,即内存中不存在与MyApp.class
相对应的Class
对象。此时,JVM会使用类加载器,将MyApp.class
加载到内存中。调试发现,此时使用的类加载器为AppClassLoader
。如果类加载器无法在classpath中找到MyApp.class
,JVM将抛出NoClassDefFoundError
。
链接
主类被加载过后,JVM会对加载的类进行链接。链接过程主要包括验证、预处理和解析。
- 验证。首先,JVM会验证加载的
MyApp
类是否是格式良好的、是否具有适当的符号表、以及是否遵循Java编程语言和JVM语义规范。 - 预处理。其次,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)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。