类加载的过程分为:加载、验证、准备、解析和初始化五个阶段。
加载
加载是类加载过程中的一个阶段,切勿混淆。
加载需要完成三件事:
1、通过类的全限定类名来获取类的二进制字节流。
2、将字节流代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区该类的数据访问入口。
对于非数组类型来说,既可以使用虚拟机内置的引导类加载器来完成,也可以用用户自定义的类加载器完成。
对于数组类型来说,数组类本身不通过类加载器创建,但数组的元素类型还是要通过类加载器创建。
数组类的创建应该遵循以下规则:
(1)如果数组的组件类型是引用类型,则递归采用加载过程去加载该组件,数组将被标识在所用的类加载器的名称空间上。
(2)如果不是引用类型,数组会被标记为与引导类加载器关联。
(3)数组类的可访问性与组件类型一致,如果不是引用类型则默认为public。
值得注意的是:加载阶段与连接阶段部分内容是交叉进行的,但开始时间有先后顺序。
验证
验证是链接阶段的第一步,目的是确保Class文件中的字节流信息符合《Java虚拟机规范》的全部约束要求。
验证阶段包括四个检验动作:
(1)文件格式验证:验证字节流是否符合Class文件格式的规范。
(2)元数据验证:对字节码描述的信息进行语义分析,主要是对类的元数据信息进行语义校验。
(3)字节码验证:通过数据流分析和控制流分析,确定程序语义合法、符合逻辑。这阶段要对类的方法体进行校验分析。
如果,类的方法体没通过字节码验证那么它一定有问题,但是如果类通过了该验证也不一定是安全的。
(4)符号引用验证:该阶段发生在虚拟机将符号转化为直接引用的时候,转化发生在解析阶段。该阶段验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。目的是确保解析阶段能正常执行,这是是一个重要但不是必要的步骤。
准备
准备是正式为类中的静态变量分配内存并设置类变量初始值的阶段。
通常这个初始值是被设为对应数据类型的零,特殊情况是类变量被修饰为final时,则该对象会被直接复制为定义的值。
解析
解析是将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号(可以是任何格式的字面量,无歧义即可)来描述所引用的目标。
直接引用:可以直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
初始化是类加载过程的最后一步。前面的几个类加载动作里,除了加载阶段用户程序可以通过自定义类加载器局部参与,其他都完全由JVM主导控制。到了初始化阶段,JVM才开始执行Java程序代码,将主导权交回应用程序。
初始化阶段可以看做是执行类的构造器<clinit>()方法的过程。
<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。如下所示:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.println(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
<clinit>() 方法不需要显式调用父类构造器,虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕,因此第一个执行该方法的一定是java.lang.Object。
由于父类的 <clinit>() 方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;//B的值会是2,不是1,A=2线执行,再执行B=A。
}
<clinit>() 方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法。
接口中不能使用静态代码块,但接口也需要通过 <clinit>() 方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的 <clinit>() 方法不需要先执行父类的 <clinit>() 方法,只有当父接口中定义的变量使用时,父接口才会初始化,接口的实现类初始化时同理。
虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法。