之前我们看了Class文件中有那些内容,现在JVM如何加载这些Class文件呢?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机使用的java类型,这就是虚拟机的类加载机制。
java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略会稍微增加一些性能开销,但会为java应用提供高度的灵活性,Java语言里面的特性就是依赖运行期动态加载和动态连接这个特点实现的。
类加载的时机
类从被加载到JVM内存中开始,到卸载出内存的时候,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。
其中 验证、准备和解析统称为连接。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。类加载过程必须按照这五个步骤按部就班的开始,而解析阶段则不一定。可以在初始化之后(动态绑定)。
那么什么情况下需要进行第一步加载的操作?
JVM并没有强制的约束,可以交给JVM自由把控,但是对于初始化阶段,JVM规范则是严格规定了只有五种情况立刻对类进行初始化。
1 遇到new , getstatic , putstatic , invokestatic这四条指令的时候,如果类还没有进行初始化,则需要先触发初始化。生成这四个指令最常见的java代码场景: 使用new 关键字实例化对象的时候,读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类静态方法的时候。
2 使用Java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则先触发初始化。
3 当加载一个类时,如果发现它的父类还没有初始化,则先对它的父类进行初始化。
4 当虚拟机启动时,用户需要指定一个执行的主类(包含main方法的那个类),JVM会先初始化这个类
5 使用jdk1.7时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果REF_getStatic、REF_putStatic、REF_invokestatic的方法句柄,并且这个方法的句柄没有初始化,则需要先触发初始化。
对于这五种方式触发初始化的场景,JVM规范中使用了一个很强烈的限定语:有且只有
对于这五种场景的行为成为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化。成为被动引用。
接口的加载过程
接口的加载过程和类有些不同,接口也有初始化过程,这个和类是一样的。当一个接口在初始化时,并不要求父接口全部完成了初始化,只有在真的使用到父接口时,才会初始化。
类加载过程
加载时类加载(Class Loading)过程的一个阶段。
在加载阶段,需要完成3个步骤
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化成方法区运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载完成以后,JVM外部的二进制字节流就按照JVM所需要的格式存储在方法区之中,方法区中的数据存储格式由JVM自己定义,JVM规范未规定此区域数据具体结构。然后在内存中实例化一个java.lang.Class对象(并未明确规定在java堆中,对于HotSport虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面。),这个对象作为程序访问方法区中这些类型数据的外部接口。
加载阶段和连接阶段的部分内容(如一部分字节码文件格式校验动作)是交叉进行的,加载尚未完成,连接阶段可能已经开始了,但这些夹杂了加载阶段的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持固定的先后顺序。
验证
验证是连接的第一步,这一阶段是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。
验证阶段的是否严谨,直接决定了JVM是否能承受恶意代码的攻击,从执行性能的角度上看,验证阶段的工作量在JVM的类加载子系统中又占了相当大一部分。
验证阶段大致分为四个阶段校验动作 :文件格式验证、元数据验证、字节码验证、符号引用验证
1 文件格式验证
第一阶段要验证字节流是否符合Class文件规范,并且能够被当前版本的虚拟机处理。
只有通过了这个验证,字节流才能进入内存的方法区中进行存储,所以后面验证都是基于方法区的存储结构进行的,不会直接操作字节流
2 元数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。
第二个阶段的目的是:对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
3 字节码验证
第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流进行分析,确定程序的语义是合法的、符合逻辑的。
4 符号引用验证
最后一个阶段验证发生在JVM将符号引用转化为直接引用的时候,这个转化动作将在连接第三个阶段---解析阶段中发生。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,这些变量所使用的内存都将在方法区中进行分配,这个时候进行内存分配仅包括类变量(被static修饰变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中。其次,这里所说的初始值通常情况下指的是数据类型的零值。
解析
解析阶段是JVM将常量池内的符号引用转化成直接引用的过程
符号引用 : 符号引用以一组符号来描述所引用的目标,符号可以是任意类型的字面量,只要使用时能无歧义的定位到目标即可。符号引用和JVM实现的布局无关
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和JVM实现的布局有关
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应了常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7中常量类型。
1 类或接口的解析
假设当前代码处于D,把一个从未解析过的符号引用N解析成为一个类或者接口C的直接引用,那么会有以下几个操作
(1) 如果C不是一个数组类型,那么JVM会把代表N的权限定名传递给D的类加载器去加载C这个类,在加载的过程中,由于元数据验证的需要,又可能触发其他的类加载操作,例如加载这个类的父类或实现接口出现异常,解析就失败了
(2) 如果C是一个数组类型,并且数组的元素类型为对象(比如Integer),也就是N的描述符类似与[Ljava/lang/Integer],那么会按照1的规则加载数组元素类型,接着由JVM生成一个代表数组维度和元素的数组对象。
(3) 如果前两步没有异常,那么C实际上已经是一个有效的类或者接口了,但是在完成解析的时候还要进行符号引用验证,确认D是否有C的访问权限,如果不具备则抛出异常(java.lang.IllegalAccessError)
2 字段解析
要解析一个未被解析的字段符号引用,首先会对表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了异常,都会导致字段符号引用失败。如果解析成功完成,那将这个字段所属的类或接口C表示
3 类方法解析
类方法解析步骤和字段解析第一个步骤一样,如果成功用C表示这个类,然后按照JVM的规则进行类方法搜索
4 接口方法解析
同上
初始化
类初始化是类加载的最后一步,到了类初始化阶段才真的执行类中定义的Java代码。
在准备阶段,变量已经赋值过一次初始值了,而在初始化阶段,则根据程序员通过程序定制的主观计划去初始化变量和其他资源,或者从另外一个角度:初始化阶段是执行类加载器<clinit>()方法的过程。
<clinit>()方法: 由编译器自动收集类中所有变量的赋值动作和静态语句块中语句合并产生,编译器收集的顺序由语句在源文件出现的顺序决定,静态语句块中只能访问到定义的静态语句块之前的类变量,定义在它之后的类变量,在前面的静态语句块可以赋值但是不能访问。