1. 概述
一个.java文件编译为.class文件后才可以被加载到虚拟机中运行和使用.
虚拟机把描述类的.class文件加载到内存, 并对class文件进行验证、准备、解析和初始化后, 最终形成可以被虚拟机直接使用的Java类型, 这就是虚拟机的类加载机制.
2. 类加载的时机
类从加载到虚拟机内存中到卸载出内存为止。它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
类加载的顺序即按照此顺序开始,这些阶段通常互相交叉式地混合进行,通常在一个阶段的执行过程中激活下一个阶段。但是某些情况下解析阶段会在初始化后进行(比如在运行时动态的解析某些方法的符合引用,多态的使用等等);
虚拟机规范严格规定了5种情况下必须对类进行初始化,这五种情况称为类的主动引用,除此之外的其他方式都不会触发类的初始化,称为被动引用。
2.1 主动引用
- 遇到new(创建对象)、getstatic(读取类静态变量)、putstatic(设置类静态变量)、invokestatic(调用静态方法)这四条字节码指令时,如果类未进行初始化,则触发其初始化;
- 使用 java.lang.Reflect 包的方法对类进行反射调用的时候;
- 当初始化一个类时,其父类还未初始化,则需要先触发父类的初始化; 接口初始化时,只有在真正用到父接口时才初始化父接口
- 当虚拟机启动时,虚拟机会先初始化要执行的主类;
- 如果 java.lang.invoke,MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时,若此类未进行初始化,则先触发其初始化。
2.2 被动引用
- 通过子类调用父类的静态变量,若父类未进行初始化则会触发父类的初始化,而不会触发子类的初始化;
- 通过数组定义类引用;
- 引用常量。
3. 类加载过程
类加载过程一般就是加载、验证、准备、解析、初始化五个阶段。
3.1 加载
加载阶段主要完成3件事:
- 通过类的全限定名来获取定义此类的二进制字节流;
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表此类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
3.2 验证
确保class文件的字节流包含的信息符合当前虚拟机的要求,不会为危害虚拟机的自身安全。
- 文件格式验证:直接操作字节流,保证输入的字节流能正确的解析并存储于方法区,格式上符合一个Java类型的要求;
- 元数据验证:对类的元数据进行语义校验,保证其描述的信息符合Java语言规范的要求;
- 字节码验证:通过数据流和控制流分析,确保语义是合法的,符合逻辑的;
- 符号引用验证:确保解析动作能正常执行,发生在将符号引用转化为直接引用的时候。
3.3 准备
为类变量分配内存空间,并设置类变量的初始值,这些变量的内存分配都在方法区中进行。
初始化类的方法表。
static final 修饰的变量直接初始化为指定的值。
3.4 解析
将常量池中一部分符号引用转化为直接引用的过程。
符号引用:一组用来描述引用目标的符号。
直接引用:直接指向目标的指针,相对偏移量或能间接定位到目标的句柄。
在类加载的解析阶段将符号引用解析为直接引用的前提是:方法在程序真正运行之前就有一个可确定的方法调用版本,并且在 运行期间是不会改变的。
Java虚拟机提供了五种方法调用的字节码指令:
- invokestatic:调用静态方法
- invokespecial:调用构造方法,私有方法和父类方法
- invokevirtual:调用所有的虚方法
- invokeinterfafce:调用接口方法,运行时确定一个实现此接口的对象
- iinvokedynamic:运行时动态解析出调用点所引用的方法,然后执行该方法。
能被 invokestatic 和 invokespecia 调用的方法都可以在解析阶段被转化为直接引用,这类方法被称为非虚方法(final 修饰的方法也是非虚方法),其他方法称为虚方法。
解析调用是一个静态的过程,在编译期间就可以完全确定。
分派调用则可能是静态的也可能是动态的,还可以根据分派依据的宗量数分为单分派和多分派。
重载:编译期,静态多分派,根据参数的静态类型确定方法的使用版本
重写:运行期,动态单分派,根据对象的实际类型确定方法的使用版本
3.5 初始化
根据<clinit>方法为所有的类变量进行赋值,并执行静态代码块。
<clinit>是由编译器自动收集类中的所有类变量的赋值动作(准备阶段JVM为类变量只分配了初始值)和静态语句块;