之前我们了解过class文件的存储格式,本文将介绍虚拟机如何加载class文件。
一般来说,类的生命周期被划分为加载、验证、准备、解析、初始化、使用和卸载几个阶段,其中验证、准备和解析被统称为连接阶段。解析的过程顺序不确定,可能在初始化开始之后才开始,这常常出现在Java的晚绑定(动态绑定)中。
“加载”和“类加载”听起来十分相似,但是加载只是类加载的一个阶段。加载阶段的三个任务:
java.lang.Class
对象,作为方法区数据结构的访问入口。对于第一个任务,Java虚拟机规范并没有明确要求从字节码文件中获取二进制字节流,因此一般可以从以下途径获取:
非数组类型的加载阶段是程序员可控制性最强的阶段,程序员可以通过自定义类加载器来控制字节流的获取方式。
数组类型本身并不是通过类加载器进行创建,而是通过JVM在内存中直接动态创建。但数组类的元素类型(去掉所有数组维度的类型)最终还是要靠类加载器加载,数组类的创建过程遵循下列规则:
验证阶段保证class文件的字节流符合Java虚拟机规范,不会损害Java虚拟机的安全。验证阶段主要包括四个验证动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
文件格式验证是要验证字节流是否符合class文件格式规范,可以验证的地方主要包括:
主要包括以下验证点:
前面的元数据验证主要是用于数据类型校验,字节码校验是进行类的方法体校验,包括以下内容:
符号引用验证主要是看该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。主要验证下列内容:
这个阶段是为静态变量分配内存并设置初始值的阶段,一般来说是应该在方法区上进行操作的,在JDK7之前,HotSpot使用永久代来实现方法区,但JDK8开始,永久代被移除,静态变量就和class对象一起被存放在了堆空间中。因此,静态变量是在逻辑上的方法区。
准备阶段的初始值并不是代码中赋予的值,而是“赋零值”的过程,一起看一下下面的代码:
public static int value = 1;
尽管代码里面写的是给value赋1,但准备阶段应该是将0赋给value,真正将1赋给value的阶段应该是后面的初始化的阶段。下面列出了各种数据类型的零值。
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
然而并不是所有情况都是赋零值的过程,当静态变量被final修饰时,例如下面这种情况:
public static final int value = 1;
这种情况下,value在编译阶段就已经生成了ConstantValue属性,所以在准备阶段就会把1赋给value。
解析阶段就是将常量池中的符号引用转换为直接引用的过程。前面提到了符号引用相关的内容,这里简要说明一下符号引用和直接引用的概念。
符号引用:符号引用是一组用符号来描述所引用的目标,这些符号可以无歧义地定位到目标。在前面讲类文件结构时就提到过符号引用如CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info等。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
初始化是类加载过程的最后一个阶段,前面的准备阶段我们说这是一个给静态变量赋零值的阶段,而到了初始化阶段就是真正给Java代码中的静态变量赋初始值的时候了。在初始化阶段,javac编译器会生成一个
方法,用来给静态变量和静态语句块赋值。
必须保证在多线程场景下被正确加锁同步,所以多个线程去执行
方法时可能会遇到耗时操作导致阻塞。下面的代码进行了这种情况的演示。
public class TestDeadLoopClass {
static class DeadLoopClass {
static {
//这里不加if语句会无法编译
if (true) {
System.out.println(Thread.currentThread() + " init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + " start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
运行结果如下,一个线程卡死了,另一个线程一直阻塞:
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main] init DeadLoopClass
接下来看一下初始化的几种情况:
1.遇到new、getstatic、putstatic或invokestatic时触发初始化:
java.lang.reflect
方法对类型进行反射调用的时候,如果类型没有初始化则先进行初始化。java.lang.invoke.MethodHandle
实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,且方法句柄对应的类没有被初始化,则先进行初始化。