类加载的整个过程如下图所示:包括加载,验证,准备,解析,初始化,使用,卸载整个流程。
一、加载
目的:通过一个类的全限定名来获取定义此类的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
其中获取Class文件可以有如下途径:
1.从zip压缩包中读取;
2.从网络中获取;
3.运行时计算生成;
4.由其他文件生成;
5.从数据库中读取;
6.可以从加密文件中获取
二、验证
目的:确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束。
过程:验证主要包含四个验证过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
1.文件格式验证:
1.是否以魔数0xCAFEBABE开头;
2.主、次版本号是否在当前Java虚拟机接受范围之内;
3.常量池的常量中是否有不被支持的常量类型(检查常量tag标志);
4.指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;
5.CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据;
6.Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
2.元数据验证
1.这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类);
2.这个类的父类是否继承了不允许被继承的类(被final修饰的类);
3.如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法;
4.类中的字段、方法是否与父类产生矛盾(如:覆盖了父类的final字段,或者出现不符合规则的方法重载等)
3.字节码验证
1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(如:不会出现类似于“在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况);
2.保证任何跳转指令都不会跳转到方法体以外的字节码指令上;
3.保证方法体中的类型转换总是有效的(如:可以把一个子类对象赋值给父类数据类型,但不能把一个父类对象赋值给子类数据类型)
4.符号引用验证
1.符号引用中通过字符串描述的全限定名是否能找到对应的类;
2.在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段;
3.符号引用中的类、字段、方法的可访问性是否可被当前类访问
三、准备
目的:是正式为类中定义的变量(静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。注意:此时只是赋予初始化值,并没有真正的进行赋值操作;例如:
public static int a = 1
在准备阶段a的值是初始值0,并不是1,赋值操作是在初始化进行的。所有类型的默认值如下所示:
数据类型 | 初始值 |
int | 0 |
long | 0L |
short | (short)0 |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
四、解析
目的:java虚拟机将常量池内的符号引用替换为直接引用的过程。其中符号引用和直接引用解释如下:
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
解析作用主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。
五、初始化
目的:是真正执行类中编写java程序代码的过程;就是执行类构造器<clinit>()的过程。
说明:<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。执行<clinit>()方法时,不需要再去执行父类的类构造器,因为虚拟机保证了在执行子类的类构造器之前,父类的类构造器一定是已经执行完的,所以第一个执行的类构造器是java.lang.Object,所以父类中的静态代码块先于子类执行;<clinit>()方法,并不是每一个类或接口都有的方法,当一个类中没有静态变量和静态代码块时,编译器就不会为该类或接口生成此方法;