类加载过程包括 5 个阶段:加载、验证、准备、解析和初始化。
“加载”是“类加载”过程的一个阶段,不能混淆这两个名词。在加载阶段,虚拟机需要完成 3 件事:
对于 Class 文件,虚拟机没有指明要从哪里获取、怎样获取。除了直接从编译好的 .class 文件中读取,还有以下几种方式:
loadClass()
方法)。验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备阶段是正式为类变量(或称“静态成员变量”)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。
初始值“通常情况下”是数据类型的零值(0, null…),假设一个类变量的定义为:
public static int value = 123;
那么变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法。
存在“特殊情况”:如果类字段的字段属性表中存在 ConstantValue 属性,那么在准备阶段 value 就会被初始化为 ConstantValue 属性所指定的值,假设上面类变量 value 的定义变为:
public static final int value = 123;
那么在准备阶段虚拟机会根据 ConstantValue 的设置将 value 赋值为 123。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
类初始化阶段是类加载过程的最后一步,是执行类构造器 <clinit>()
方法的过程。
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。如下方代码所示:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.println(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
<clinit>()
方法不需要显式调用父类构造器,虚拟机会保证在子类的 <clinit>()
方法执行之前,父类的 <clinit>()
方法已经执行完毕。
由于父类的 <clinit>()
方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下方代码所示:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出 2
}
<clinit>()
方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 <clinit>()
方法。
接口中不能使用静态代码块,但接口也需要通过 <clinit>()
方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的 <clinit>()
方法不需要先执行父类的 <clinit>()
方法,只有当父接口中定义的变量使用时,父接口才会初始化。
虚拟机会保证一个类的 <clinit>()
方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>()
方法。