引言
类加载的全过程分为5个阶段:加载,验证,准备,解析,初始化。
加载
加载阶段虚拟机需要完成3件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流,获取方式很多种如Class文件、网络、运行时计算生成等。
2)将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表该类的java.lang.Class对象放在方法区,作为该类各种数据的访问入口。
一个非数组类的加载可以由引导类加载器完成也可以由用户自定义的类加载器完成。对于数组类而言,数组本身不通过类加载器创建,是虚拟机直接完成。
一个数组的创建遵循如下规则:
如果数组的组件类型(去掉一个维度)为引用类型,递归加载该引用类型,数组将在加载该组件类型的类加载器的类命名空间上被标记。
如果数组的组件类型不是引用类型(如int[]),虚拟机将会把数组与引导类加载器相关联。
数组的可见性与其组件的可见性一致,如果数组的组件类型不是引用类型(如int[])默认可见性为public。
验证
验证是连接的第1步,作用是保证Class文件的字节流中包含的信息符合虚拟机的要求。
文件格式验证
验证字节流信息是否符合Class文件格式规范。
1)是否以魔数开头。
2)主次版本号是否在当前虚拟机处理范围之内。
3)常量池中的常量类型是否被支持(检查常量tag标识)。
。。。。。。
只有通过了该阶段的验证,字节流才能进入方法区中进行存储。该阶段是针对字节流,后三个阶段是针对方法区的存储结构的。
元数据验证
对字节码描述的信息进行语义分析,保证 其描述的信息符合Java语言规范。
1)该类是否有父类(除Object其他类都应有)。
2)该类父类是否继承了不能被继承的类(final修饰的类)。
。。。。。。
字节码验证
该阶段是对方法体进行校验分析,确保方法运行时不会危害虚拟机。
1)保证任意时刻操作数栈的数据类型和指令代码序列配合工作,如不允许在操作数栈放了一个int类型数据,运行时却按long类型加载到局部变量表中。
2)保证跳转指令不会跳到方法体以外的指令上。
3)保证类型转换是有效的。
。。。。。。
字节码验证阶段很复杂,此时StackMapTable发挥作用。
符号引用验证
该阶段的校验发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),确保解析能正常执行。
1)符号引用中字符串描述的全限定名能否找到对应的类。
2)符号引用中的类、字段、方法是否能被当前类访问。
。。。。。。
准备
该阶段为类变量(static修饰)分配内存并设置初始值。类变量所使用的内存是在方法区中分配的,初始值通常情况下是数据类型的零值。
如 public static int val = 123;准备阶段的初始值为0而不是123。
特殊情况是类字段属性表中ConstantValue属性,准备阶段赋初始值为ConstantValue属性值。
如 public static final int val = 123;准备阶段的初始值为123而不是0。
解析
解析阶段是将常量池中的符号引用转化为直接引用的过程。
类或接口的解析
被引用的类(普通实例或数组)执行加载动作,解析完成之前进行符号引用验证。
字段解析
对字段所属的类或接口解析,解析完成后进行字段搜索,如果类本身有该字段返回直接引用,否则如果类实现了接口则自下而上递归搜索接口字段找到返回直接引用,否则如果类不是java.lang.Object则自下而上递归搜索父类字段找到返回直接引用,否则抛异常。成功返回直接引用后进行符号引用验证。
类方法解析
对方法所属的类或接口解析,解析完成后进行方法搜索,如果发现是接口抛出异常,自身查找,递归父类查找,递归接口查找找到说明类是一个抽象类抛出异常。
接口方法解析
对方法所属的类或接口解析,解析完成后进行方法搜索,如果发现是类抛出异常,自身查找,递归父接口查找直到java.lang.Object。
初始化
该阶段开始真正执行类中定义的Java程序代码(字节码)。初始化阶段是执行<clinit>方法的过程(执行静态代码块内容或给静态变量赋值)。
<clinit>()方法是由编译器自动收集类中所有类变量赋值动作和静态代码块的语句合并生成的。静态代码块只能访问块之前的类变量,可以为块之后的类变量赋值。
static { val = 2; System.out.println(val); Cannot reference a field before it is defined } static int val = 1;
<clinit>()方法与<init>方法不同,前者不需要显示的调用父类构造器,前者对于类或接口来说并不是必须的,没有静态代码块或未给静态变量赋值时不会产生<clinit>()方法。