类加载过程包括加载、验证、准备、解析、初始化,下面将对这5个阶段进行详细的学习。
1.加载(重点)
加载是将二进制字节流加载到jvm方法区中,并生成一个Class对象,作为类的访问入口,在这一阶段主要完成的工作如下:
通过类的全限定名获取类的二进制字节流
(没有限定二进制字节流就是class文件,所以可以通过多种方式获取二进制字节流,如文件,网络,数据库等)将字节流所代表的静态存储结构转换为运行时数据结构
- 在内存中生成代表这个类的java.lang.Class对象,(对于hotspot而言,虽然是对象实例,但是其存于方法区中)作为方法区该类的各种数据的访问入口。
加载阶段可以通过用户自定义加载器去加载二进制字节流,可控性强。加载完成后,二进制字节流就按照虚拟机要求的格式存放在方法区中。
(补充)方法区运行时数据结构
(1)类型信息
主要存放:
类的全限定名
父类的全限定名
接口的全限定名列表
类的修饰符
如果该类为java.lang.Object类,则没有父类和接口。
(2)常量池
jvm为每个已加载的类型维护一个常量池,对应class文件中的常量池。
常量池包含的信息由字面量以及字段、方法、类的符号引用。
在动态链接中起到核心作用。
(3)字段信息
保存类型的所有成员变量(实例变量、静态变量)的相关信息,包括字段名,字段类型,字段的访问修饰符。
(4)方法信息
包括类的所有方法信息
包括方法修饰符,方法返回类型,方法名,方法参数列表。(除抽象方法和本地方法外)、字节码、操作数栈、局部变量区大小,异常表。
(5)类变量
静态变量,即使没有实例变量,也可以访问类变量,与类关联。
(6)指向类加载器的引用
保存加载此类的加载器
(7)指向Class实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
(8)方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的。
(9)运行时常量池
包括常量池的常量,运行时新产生的常量。
2.验证
在加载阶段已经涉及到验证了,验证用来确保二进制字节流不会危害到jvm系统安全。jvm通过将二进制字节流加载到方法区中,但是没有限定二进制字节流的来源,用户可以修改有害文件为二进制字节流class文件,如果虚拟机不进行验证,对jvm系统危害大。
验证主要包括以下四方面:
(1)文件格式验证(符合class文件规范)
文件格式验证主要验证二进制字节流是否符合class文件格式规范,以及能否在当前虚拟机上运行。验证内容有以下:
是否已魔数开头;
主、次版本号是否在当前虚拟机处理范围内;
常量池中的常量是否有不被支持的类型;
…..
文件格式验证主要用来确保二进制字节流为合法的class文件,能被当前虚拟机处理,此阶段验证完成后,二进制字节流才能进入方法区中,(所以在加载阶段,此阶段就已开始)后面的验证都是基于方法区的数据结构进行的。(第一步确保能进入方法区)
(2)元数据验证(符合java语言规范)
这一部分主要用来验证方法区中描述的类是否符合java语言规范,主要有:
此类是否有父类(除了Object类,其他类均有父类)
此类是否继承了不该被继承的类(被final修饰的类)
此类如果不是抽象类,是否实现了其父类或接口中要求实现的方法
……..
此阶段是对类的整体进行验证
(3)字节码验证
上一阶段是对元数据进行验证,此阶段对方法体进行验证,主要对数据流和控制流进行验证,确保方法执行时不会做出对虚拟机有害的操作。
保证跳转指令不会跳转到方法体以外的字节码指令上。
此阶段是对方法体的验证
…….
(4)符号引用验证
此阶段发生在将符号引用转换为直接引用的时候,(即解析动作时发生此验证),主要验证能否将符号引用转换成直接引用。
此阶段验证主要用来确保解析动作的正确发生。
3.准备
准备阶段主要是为类变量分配内存并初始化值的过程,初始化的值是数据类型的默认值,对于常量,在准备阶段就将字段表中的ConstantValue属性值作为其初始值。
4.解析
解析阶段主要是将符号引用转换为直接引用(符号引用是字面量,与内存布局无关,直接引用是指向内存地址)。
解析阶段主要包括类或接口解析、字段解析、类方法解析、接口方法解析。
(1)类或接口解析
假设当前代码所处类为D,如果要将一个从未解析过的符号引用N解析为类或接口的直接引用C,将经历下面的步骤:
a、如果C不是一有个数组类型,将N传递给D的类加载器去加载C
b、如果C是数组类型,按照a加载数组元素类型,由jvm生成代表数组维度和元素的数组对象。
c、确认D对C是否有访问权限
如果符号引用已经解析过,就将符号引用(类或接口的全限定名)转换成直接引用(类获接口的内存地址)
(2)字段解析
要解析一个从未解析过的字段符号引用,首先对字段表类的class_index进行解析(即字段所属的类或接口进行解析),如果解析成功,将继续对字段进行解析,主要步骤如下:
a、如果类或接口包含字段,返回直接引用
b、如果不包含,从父接口中查找,找到,返回直接引用
c、仍未找到,从父类中查找,找到,返回直接引用
d、仍未找到,失败
如果能够找到符号引用对应的直接引用,现在要验证当前类代码对字段是否有访问权限。
(编译器不允许父类、接口出现同名字段)
(3)类方法解析
与字段解析类似,先查看class_index,找到方法所属类或接口。
a、如果该方法所属为接口,将直接抛出异常(类方法和接口方法的符号引用常量类型是分开定义的)
b、本类查找
c、接口、父类中查找
最后都要验证对方法是否具有访问权限
(4)接口方法解析
与类方法解析类似,与类方法不同之处在于接口中所有方法都是默认为public,所以不存在访问权限问题。
5.初始化
初始化是执行java程序代码,包括执行static语句,以及静态变量的赋值语句。(执行clinit方法过程)
(1)clinit方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。执行顺序是由代码顺序决定。定义在静态语句块之后的静态变量,在静态语句块中可以赋值,但不能访问。
public class Test{
static{
a = 1;//编译通过
System.out.println(a);//编译出错
}
public static int a = 2;
}
(2)保证子类执行clinit方法之前,父类clinit方法已经执行
(3)如果没有静态语句块和静态变量,则无clinit方法
(4)接口不能使用静态语句块,变量默认为静态变量,与类不同,子接口执行clinit方法,不需要保证父接口clinit方法已经执行,使用时再执行。
(5)多线程环境下,只有一个线程执行clinit方法,其他线程阻塞