类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。
类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(UNloading) 七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如图所示:
在以上的七个步骤中,加载、验证、准备、初始化和卸载这五个步骤的顺序是确定的,类加载的过程必须按照这种顺序按部就班地执行。
对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须对类进行初始化:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
- 当类初始化的时候,如果发现其父类还没有进行初始化,则要先对其父类进行初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
- 当使用 JDK 7 新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有初始化过,则需要先触发其初始化。
- 当一个接口加入了 JDK 8 新加入的默认方法时(被default关键字修饰的接口方法),如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
以上六种情况称为对一个类型的主动引用,除此之外其他引用类型的方式被称为被动引用,被动引用不会触发初始化。
类加载的过程
加载
类加载过程的第一个阶段,ClassLoader通过一个类的全限定名称查找此类的字节码文件,并利用字节码文件创建一个class对象。
验证
目的在于确保class文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
准备阶段是正式为类变量(即静态变量,被static修饰的变量)分配内存空间并设置其初始值的阶段,从概念上讲,这些变量所使用的内存都应该在方法区中分配,但方法区本身是一个逻辑上的区域,在 JDK 7 及之前,hoHotspot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的,而在 JDK8 及以后,类变量则会随着Class对象一起存放在 Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。在准备阶段进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起被分配到 java堆中。我们看下面一个例子:
public static int value = 111;
需要注意的是,静态变量value在准备阶段过后的初始值是 0 而不是 111,因为这时尚未开始执行任何 Java方法,而把 111 赋值给 value 是在程序被编译后,所以给 value 赋值的操作要到类的初始化阶段才会执行。
解析
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
类的初始化阶段是类加载的最后一个阶段,在这个阶段,Java 虚拟机才开始执行类中编写的 Java 程序代码。进行准备阶段时,变量已经赋值过一次系统要求的零初始值,而在初始化阶段,则会根据程序员通过程序编程制定的主观计划去初始化类变量和其他资源。
类加载器
实现类加载功能的代码称为类加载器
类与类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java虚拟机中的唯一性。
双亲委派模型
图中展示的各种类加载器之间的层次关系就是双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器。不过这里所指的父子关系不是继承关系,通常是使用组合关系来复用父类加载器的代码。
双亲委派模型工作过程
如果一个类加载器收到了类加载的请求,它首先不会去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈给子加载器说自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
双亲委派模型的优点
双亲委派模型的好处就是 Java 类随着它的类加载器一起具备了一种有优先级的层次关系,通过这种层次关系可以避免类的重复加载。当父加载器已经加载了该类的时候,子加载器就没有必要再加载一次。
双亲委派模型的实现
protected synchronized Class<?> loadClass (String name, boolean resolve)
throws ClassNotFoundException {
//首先,检查请求的类是否已经加载过了
Class c = findLoadedClass (name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass (name, fasle);
} else {
c = findBootstrapClassOrNull (name);
}
} catch (ClassNotFoundException e) {
//如果父类加载器抛出ClassNotFoundException异常
//说明父类加载器无法完成加载请求
}
if (c == null) {
//在父类加载器加载时
//再调用本身的findClass方法来进行类加载
c = findClass (name);
}
}
if (resolve) {
resolveClass (c);
}
}