在Java虚拟机规范中,把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class对象,这个过程被称作类加载过程。一个类在整个虚拟机周期内会经历如下图的阶段,从加载到初始化就是类加载过程。

 

一、加载阶段

这里的加载是整个类加载过程中的一个阶段,不等同于类加载,在加载阶段,会做以下三件事:

1、 通过类的全限定名读取类的二进制流。

2、 将字节流所代表的的静态存储结构转化为方法区的运行时数据结构。

3、 在虚拟机内存的堆区中生成一个代表这个类的java.lang.Class对象,用于方法区这个类的各种数据的访问入口(如下图所示)。

 

由于Java虚拟机对加载class文件的来源并未做限制,所以出现了以下的class文件加载方式:

1、 从本地系统中直接获取
2、 从网络中获取,如:Web Applet
3、 从zip压缩包中获取,将zip压缩后缀改为.jar,也可以直接使用
4、 动态代理生成
5、 由其他文件生成,如JSP
6、 从数据库中获取
7、 加密文件中获取,如Class文件加密防反编译

二、链接阶段

在加载阶段完成之后,class文件的类信息数据就会存储在方法区,同时在Java虚拟机堆区生成一个对应类的Class对象,这个Class对象会在之后变成程序访问方法区中的类数据的外部接口。链接阶段并不是一定等到加载阶段完成后才开始,链接的部分动作会跟随加载阶段进行(如部分字节码文件格式的验证动作)。

1、验证

验证是链接的第一个阶段,这个过程中,JVM会去校验class文件格式及class文件二进制流中所包含的信息是不是符合虚拟机规范的约束。包含四部分内容的验证:

文件格式验证:验证class文件魔数值是否为0xCAFEBABE、主次版本号、常量类型等。

元数据验证:对类的元数据信息进行语义校验。

字节码验证:通过数据流分析和控制流分析、确定程序语义是合法的、符合逻辑的。

符号引用验证:验证发生在解析阶段,主要对常量池中的各种符号引用进行匹配性校验。

2、准备

 

在准备阶段,会给类变量(被static修饰的静态变量)分配内存并且初始化类变量初值(零值),如上表就是各种类型对应的零值,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但是方法区本身是一个逻辑上的区域,在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候"类变量在方法区" 就完全是一种对逻辑概念的表述;

注意:

1、final修饰的类变量(常量)并不会进行准备阶段进行赋初值的操作,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值。

2、正因为类变量拥有赋初值这一操作,所以只声明类变量,不进行赋值动作,程序也能正常执行,如下代码可以验证各种类型的初值。

public class ClassLoaderPrepare {
    public static int i;
    
    public static void main(String[] args) {
        System.out.println(i);
    }
}

3、解析

解析阶段的作用是将符号引用转为直接引用。每个class文件都对应一个常量池,常量池中存储了类、接口、字段、方法等各类信息,符号引用是一组符号指向常量池中被引用的目标,要在虚拟机中定位到目标,就需要指向对应目标的内存地址,这种引用就是直接引用。

三、初始化阶段

在链接阶段的准备阶段中,已经为类变量分配了内存地址和初值,在初始化阶段就会对这些类变量进行赋值操作。如果一个类含有静态变量或者静态代码块,java虚拟机就会在编译为其生成一个方法(类初始化方法),其内容由编译期间虚拟机收集到的类变量的赋值动作和静态代码块合并而来。

注意:

1、<clinit>方法中,指令的顺序是依据指令对应的语句在源文件中出现的顺序,静态代码块中只能访问定义在它之前的变量,如下代码就会提示非法的前向引用。

2、在继承关系中,父类的<clinit>方法先于子类执行。

3、在多线程同时初始化一个类时,只有其中一个线程能够执行<clinit>

public class ClassLoaderCLInit {
    static {
        i = 10;
        System.out.println(i);
    }
    public static int i;
}