虚拟机把Class文件加载到内存,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制,其中的Class文件除了存于磁盘中的文件,以其他形式存在也可以,具体指一串二进制的字节流。
1、生命周期
类从被加载到虚拟机内存到卸载出内存,整个生命周期为:加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备和解析部分统称为连接。其中加载、验证、准备、初始化和卸载必须按顺序开始(只是开始),解析可以在初始化之后再开始用于支持动态绑定。
2、类加载过程
2.1 加载
虚拟机外部的二进制字节流按照虚拟机需要的格式存储在方法区中,方法区中的存储格式由虚拟机自行定义,然后在内存中实例化一个Class对象(HotSpot虚拟机Class对象不存放在堆而是在方法区中)。在加载阶段,虚拟机需要完成3件事:
1、通过一个类的全限定名获取定义此类的二进制字节流
2、将这字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
非数组类加载阶段获取类的二进制字节流的动作,可以使用系统提供的引导类加载器完成,也可以由用户自定义的类加载器完成。对于数组类本身不通过类加载器创建,是由Java虚拟机直接创建的,不过数组类的元素类型还是由类加载器去创建的。数组类创建过程需要遵循以下规则:
1、如果数组的组件类型(数组去掉一个维度的类型)是引用类型,则需要获取组件类的全限定名去获取Class对象,该数组将在加载该组件类型的类加载器的类名称空间上被标识(二维数组?)。
2、如果数组的组件类型不是引用类型,JVM会把该数组标记为与引导类加载器关联。
3、数据类的可见性和它的组件类型的可见性一致(一致?),如果组件类型不是引用类型,数组类的可见性将默认为public。
2.2 验证
验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要有下面4个阶段:
1、文件格式验证:
验证字节流是否符合Class文件格式的规范,确保输入的字节流能正确解析并存储在方法区中,比如检验前4个字节的魔数是否正确。通过这个验证后,字节流进入方法区存储,后面的验证基于方法区的结构进行。
2、元数据检验:
对类的元数据信息进行语义检验,保证其符合Java语言规范。比如检验类是否继承了final修饰的类。
3、字节码验证:
对类的方法体进行检验,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。比如类型转换不可把对象赋值给没继承关系的数据类型。
4、符号引用验证:
在解析阶段中,当虚拟机将符号引用转化为直接引用时,触发符号引用验证。符号引用验证是对类自身以外(常量池的各种符号引用)的信息进行匹配性校验,比如验证根据字符串的全限定名是否可以找到对应的类,符号引用中的类、方法和字段是否可以被当前类访问。
2.3 准备
准备阶段是为类变量分配内存并设置初始值的阶段,使用的内存在方法区中分配(实例变量则在对象实例化时随着对象分配在堆中)。如果类变量是一个常量,即用static和final修饰的类变量,在准备阶段会初始化成该常量值,比如下面这个value在准备阶段会被赋值为123。
public static final int value = 123;
其他的类变量会初始化成零值。下面这个value变量在准备阶段后值为0,在初始化阶段会从类构造器
public static int value = 123;
各种类型的零值:
数据类型 | 零值 | 数据类型 | 零值 |
char | '\u0000' | float | 0.0f |
byte | (byte)0 | double | 0.0d |
short | (short)0 | boolean | false |
int | 0 | reference | null |
long | 0L |
2.4 解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用有7种,类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符,分别对应Class文件常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandler、CONSTANT_InvokeDynamic_info7中常量类型。直接引用是用于定位引用的目标在内存中的位置,可以是直接指向目标的指针、相对偏移量或是一个能间接定位的句柄,和虚拟机实现的内存布局相关。虚拟机规范没有规定解析阶段发生的具体时间,可以根据需要在类被加载器加载时解析,或是等到一个符号引用要被使用前才去解析。
对同一个符号引用进行多次解析请求时,除invokedynamic指令之外,虚拟机对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)避免重复解析,无论解析是否成功都只解析一次。对于invokedynamic指令,由于该指令是用于支持动态语言的,对应的引用称为"动态调用点限定符",需要等到程序实际运行到这条指令时才进行解析。
2.4.1 类或接口的解析
假设当前代码所处类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,需要以下几步:
1、如果C不是数组类型,虚拟机会把N的全限定名传递给D的类加载器去加载这个类,在加载过程中,由于元数据验证、字节码验证,可能会触发其他类的加载动作,比如加载其父类或实现的接口。
2、如果C是一个数组类型,并且数组的元素类型是对象,按照1的规则加载数组元素类型,然后由虚拟机生成一个代表此数组维度和元素的数组对象。
3、上面步骤完成后C在虚拟机中实际上成为一个有效的类或接口,然后进行符号引用验证,验证D是否具备对C的访问权限。
2.4.2 字段解析
解析一个未被解析过的字段符号引用,需要先解析字段所属的类或接口的符号引用,假设所属类或接口用C表示,解析成功后再进行下面步骤:
1、如果C本身就包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。
2、否则,如果C实现了接口,按继承关系从下往上递归搜索各个接口和其父接口,查看接口中是否包含了简单名称和字段描述符匹配的字段。
3、否则,如果C不是java.lang.Object,按照继承关系从下往上搜索其父类。
4、否则,查找失败。
如果查找成功返回直接引用,则进行权限验证。如果同名字段同时出现在C的接口或父类中,或是同时在自己和父类的接口中出现,编译器会报错。
2.4.3 类方法解析
类方法解析需要先解析类方法所属类或接口的符号引用,假设其类或接口用C表示,解析成功后进行类方法搜索:
1、如果C是一个接口,抛出异常。
2、在类C中查找是否有简单名称和描述符都匹配的方法,如果有则返回这个方法的直接引用,查找结束。
3、否则,在类C的父类中递归查找。
4、否则,在类C实现的接口列表和他们的父接口之中递归查找,如果查找成功,说明类C是一个抽象类,抛出java.lang.AbstractMethodError异常。
5、否则,查找失败。
如果查找成功返回直接引用,进行权限验证。
2.4.4 接口方法解析
先解析方法所属的类或接口的符号引用,如果解析成功,假设其类或接口用C表示,搜索接口方法:
1、如果C是个类而不是接口,抛出异常。
2、否则,在接口C中查找是否有简单名称和描述符都匹配的方法,如果有返回这个方法的直接引用,查找结束。
3、否则,在接口的父接口递归查找。
4、否则,查找失败。
接口中的所有方法都是public的,不存在访问权限的问题。
2.5 初始化
前面的类加载过程,除了在加载阶段应用程序可以通过自定义的加载器参与外,其动作完全由虚拟机主导和控制。在初始化阶段才真正开始执行类中定义的Java代码。虚拟机规范严格规定了有且只有5种情况必须对类进行初始化(加载、验证、准备自然在此之前)。
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要触发初始化。生成这4种指令常见场景为:使用new关键字实例化对象、读取或设置类的静态字段(被final修饰即在编译器把结果放入常量池的除外)、调用一个类的静态方法。
2、使用java.lang.refect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要触发器初始化。
3、当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
除了以上5种场景,其余被动引用都不会触发初始化。比如调用父类静态字段不会触发子类的初始化:
public class SuperClass { public static int value = 12; static { System.out.println("SuperClass init"); } } public class SubClass extends SuperClass{ static { System.out.println("SubClass init"); } } public class Test1 { public static void main(String[] args) { System.out.println(SubClass.value); } }
这段代码只会输出"SuperClass init"而不会输"SubClass init"。
下面这段代码运行后不会输出"SuperClass init",说明没有触发其初始化,但是这段代码触发了另外一个名为"[Lcom.yue.main.SuperClass"(com.yue.main是SuperClass的包名)的类的初始化。这个一个由虚拟机自动生成、直接继承java.lang.Object的子类,对用户代码来说是不合法的类名称,这个类代表元素类型为com.yue.main.SuperClass的一维数组。
public class Test1 { public static void main(String[] args) { SuperClass[] arr = new SuperClass[2]; } }
初始化阶段是执行类构造器
1、
public class Test1 { static { i = 0;//正常编译 System.out.println(i);//编译失败 } static int i = 1; }
2、
3、父类的
4、
5、接口中不能使用静态语句块,但也会生成
6、虚拟机会保证一个类的
3、类加载器
类加载阶段中"通过一个类的全限定名来获取描述此类的二进制字节流"动作是在JVM外部实现的,实现这个动作的代码模块称为"类加载器"。
对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
3.1 类加载器种类
从Java虚拟机的角度,类加载器可以分为启动类加载器(Bootstrap ClassLoader)和其他类加载器2种,启动类加载器是虚拟机自身的一部分,而其他类加载器由Java语言实现,全都继承自抽象类java.lang.ClassLoader。从开发人员角度看,系统提供的类加载器可以分为以下3种:
1、启动类加载器:
这个类加载器负责将放在
2、扩展类加载器:
这个类加载器负责加载
3、应用程序类加载器:
这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。它负责加载ClassPath上指定的类库,是程序中默认的类加载器。
3.2 双亲委托模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这个的类加载器之间的父子关系不以继承关系实现,而是用组合关系来复用父加载器代码。双亲委派模型如下图所示:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它会把这个请求委派给父类加载器完成,因为所有的加载请求都会传送到顶层的启动类加载器中,如果父加载器的搜索范围中没有找到所需的类,子加载器才尝试自己去加载。
使用双亲委派模型的其中一个好处是Java类随类加载器一起具备了一种带有优先级的层次关系。比如java.lang.Object类由启动类加载器加载,因此其他所有的类使用的都会是同一个Object类。
3.3 破坏双亲委派模型
双亲委派模型并不是强制性的约束模型,有几种大规模的非双亲委派模型情况:
1、双亲委派模型很好的解决了各个类加载器的基础类统一问题,但如果基础类又要调用回用户的代码,Java设计团队引入了线程上下文类加载器。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那这个类加载器默认就是应用程序类加载器。