类加载的时机

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。类从被被加载到虚拟机的内存开始,到卸载出内存为止,它的整个生命周期如图:

虚拟机类加载机制_虚拟机类

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语音的运行时绑定(也称为动态绑定或者晚期绑定)。注意以上所描述的只是开始的顺序,并不涉及完成顺序。

虚拟机规范规定有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准备要在此之前开始):

1、遇到new、getstatic、putstatic、invokestatic这4条字节码指令时;

2、使用java.lang.reflect包的方法对类进行反射调用的时候;

3、当初始化一个类的时候,如果发现其父类还没被初始化,则需要先触发其父类的初始化;

4、当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机先初始化这个类。

以上四种场景被称为对类进行主动引用,除此之外所有引用类的方式称为被动引用都不会触发初始化操作。

接口的加载过程过程和类稍有不同:接口也有初始化过程,但是接口中不能使用“static{}”语句块,但是编译器仍会为接口生成“<clinit>()”类构造器,用于初始化接口中定义的成员变量。接口与类正在区别是初始化一个接口的时候不要求先初始化其父接口。


类加载的过程

1、加载:

在加载阶段虚拟机完成以下3件事:

1)、通过一个类的全限定名获取类的二进制字节流;

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

3)、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法去这些数据的入口。

2、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全,验证过程失败会抛出java.lang.VerifyError异常或者其子异常。验证分为四个过程分别为:文件格式验证、元数据验证、字节码验证和符号验证。

1)、文件格式验证:魔数、主次版本号、常量池类型、是否有指向不存在常量的索引、CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据等等。

2)、元数据验证(主要验证是否符合Java语法规范):类是否有父类(除Object外)、是否继承了不允许被继承的类(final类)、如果不是抽象类是否实现了父类或者接口中需求实现的所有方法、类中的字段和方法是否和父类产生了矛盾等等;

3、字节码验证(主要验证方法体内容):保证任意时刻操作数栈的数据类型和代码序列能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令上、保证方法体重的类型转换有效等等;

4、符号引用验证:这个验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化在解析阶段中发生。符号验证可以看做是对常量池中的各种符号引用进行匹配性的校验。通常需要检验字符串描述的全限定名称是否能找到对应的类、类中是否存在符合方法的字节描述符以及简单名称所描述的方法和字段、符号阴阳的类、字段和方法的访问性是否可被当前类访问。

3、准备

准备阶段正式为类变量分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量value会被初始化ConstantValue属性所指定的值。

4、解析

解析阶段是虚拟机将常量池内的符号引用转换为直接引用的过程,其中符号引用以一组符号来描述引用的目标,直接引用可以是直接指向目标的指针和内存有关。虚拟机规范要求在执行anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前先对所使用的符号引用进行解析。解析动作主要针对类或者接口、字段、类方法、接口方法四类符号引用进行,分别对应常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。

1)、类或接口的解析:

假设当前代码所处的类为D,把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用。

A:如果C不是一个数组类型,那么虚拟机将会把代表N的全限定名传递给D的类加载器加载。

B:如果C是一个数组,并且数组的元素类型为对象,则会按照上一步规则加载数组元素类型,接着有虚拟机生成一个代表数维度和元素的数组对象。

C:如果上面的步骤没有出现异常,那么C在虚拟机中实际已经成为一个有效的类或者接口了,但在解析之前还要进行符号引用验证,确认D是否具备对C的访问权限,如果不具备权限将跑出java.lang.IllegalAccessError异常。

2)、字段解析:

字段解析首先解析字段所在的类或者接口的符号引用,解析完成后进行下面的操作:

A:如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束;

B:否则,如果在C中实现了接口,将会按照继承关系从上往下递归搜索各个接口和它的父接口,如果接口中存在匹配字段,返回直接引用查找结束;

C:否则,如果C不是Object的话,将会递归搜索其父类,如果父类有符合要求的目标,返回直接引用,查找结束;

D:否则,查找失败,抛出java.lang.NoSuchFieldError异常。

如果查找过程返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。

3)、类方法解析:

类方法解析第一个步骤和字段解析一样,也是先解析出类。

A:类方法和接口方法符号引用常量类型定义是分开的,如果在类方法中发现class_index中索引的C是个接口,抛出java.lang.IncompatibleClassChangeError异常。

B:在C类中查找名称和描述符都匹配的方法,找到返回引用,查找结束;

C:否则,在C的父类中递归查找是否有匹配的方法,找到则返回,查找结束;

D:否则,在C的接口列表以及他们的父接口中递归查找,找到匹配的说明C是一个抽象类,查找结束抛出java.lang.AbstractMethodError异常。

E:否则,查找失败,抛出java.lang.NoSuchMethodError.

最后如果返回了直接引用,将对方法进行权限验证,无权限则抛出java.lang.IllegalAccessError异常。

4)、接口方法解析