虚拟机类加载机制

it2022-05-06  1

类的加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:

加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading) 加载、验证、准备、初始化和卸载5个阶段的顺序是确定的,而解析不一定,在某些情况下可以在初始化之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。按部就班开始不一定按部就班“进行”或“完成”。

什么时候开始类加载过程的第一个阶段虚拟机并没有强制约束。但是对于初始化阶段,虚拟机规范则是严格规定类有且只有5种情况必须立即对类进行初始化。(而加载、验证、准备自然需要在此之前)

遇到new、getstatic、putstatic或invokestatic这4条指令时,如果类没有初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是(3种):使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。使用java.lang.erflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。当虚拟机启动时,用户需要制定一个要执行的主类(包含mian()方法的那个类),虚拟机会先初始化这个主类。当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最火的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化过,则需要先触发其初始化。

除了这5种(这5种场景中的行为称为对类进行主动引用)之外,所有引用类的方式都不会触发初始化,称为被动引用。 被动引用的例子

通过子类引用父类的的静态字段,不会导致子类初始化(但是父类会被初始化)单纯new一个类的数组,这个类不会被初始化,但是会有虚拟机自动创建一个直接继承Object的子类,创建的动作由字节码指令newarry触发引用被final修饰的类常量:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。(实际上都转化为对调用类自身常量池的引用)

类的加载过程

加载

类加载过程的第一步,主要完成以下3件事

通过一个类的全限定名来获取定义此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生存一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

虚拟机规范的这3点要求其实并不算具体,因此虚拟机实现于具体应用的灵活度都很大。比如”通过一个类的全限定名来获取定义此类的二进制字节流“并没有指明具体从哪里获取,怎样获取。比如常见的就是从zip包中读取(日后出现JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)、动态代理技术

一个非数组类的加载阶段(准确的说是加载阶段获取类的二进制字节流的动作)是开发人员可控性最强的,可以使用系统提供的引导类加载器来完成,也可以有用户自定义的类加载器去完成。开发人员可以通过定义自己的类加载器去了控制字节流的获取方式。 数组类型不通过类加载器创建,它由Java虚拟机直接创建。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自定义。然后在内存中实例化一个java.lang.Class类对象(并没有明确规定实在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,天气虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

加载阶段于连接阶段是交叉进行的,加载阶段尚未完成,连接阶段可以能以降开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证是连接阶段的第一步,这一阶段主要是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。 对于虚拟机的类加载来说,验证阶段是一个费城重要但不是一定必要的阶段。如果所运行的全部代码(包括第三方包中的代码)都已经被反复使用和验证过,那么可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

文件格式的验证

该验证阶段的主要目的是保证输入的字节流能正确的第解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储。

1. 字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理 2. 是否以魔数0xCAFEBABE开头 3. 主次版本号是否在当前虚拟机处理范围之内 4. 常量池的常量中是否有不被支持的常量类型(检查常量tag标志) 5. 指向常量中的各种索引值中是否有志向不存在的常量或不符合类型是的常量 6. CONSTANT_Utf8_info型常量中是否有不符合UFTF8编码的数据 7. Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。 8. ... 元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

* 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类) * 这个类的父类是否继承了不允许被继承的类(被final修饰的类) * 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。 * 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合国则的方法重载,例如方法参数都一致,但返回类型却不同等) * ... 字节码验证

是整个验证阶段最复杂的一个阶段,主要目的是通过数据流可控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据细信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事

* 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。比如不会出现,操作栈放置的类型与使用时不一致。 * 保证跳转指令不会跳转到方法体意外的字节码指令上 * 保证方法体中的类型转换是有效的。 * ... 符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段:解析阶段发生。符号引用验证的目的是确保解析动作能正常支持

* 符号引用中通过字符串描述的全限定名知否能找到对应的类。 * 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段 * 符号引用中的类、字段、方法的访问性是否可被当前类访问 * ...

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区分配。对于该阶段有以下几点需要注意:

这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中这里所设置的初始值“通常情况下”是数据类型默认的零值。特殊情况比如加了final关键字,那么准备阶段的值就是被赋予的值。 基本数据类型的零值 数据类型零值int0long0Lshort(short)0char‘\u0000’byte(byte)0booleanfalseflaot0.0fdouble0.0dreferencenull

解析

解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。

符号引用(Symbolic References) 符号引用是用以一组符号来描述所引用的目标,符号可以是renew形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中

直接引用(Direct References) 直接引用可以时直接指向目标的指针、相对编译量或是一个能间接定位到目标的句柄,直接用用是和虚拟机实现的内存布局相关的,同一个符号引用在不通虚拟机实例上翻译出来的直接引用一般不会相同。如果有量直接引用,那用用的目标必定已经在内存中存在。

除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作的重复。无论进行多少次解析,虚拟机要保证在同一个实体中,一个符号引用已经被解析成功,后续的引用解析请求就当一直是成功的,同样的,如果第一次解析失败了,那么其他指令对这个符号的解析请求也应该收到相同的异常 对于invokedynamic指令,上面的规则不成立。invokedynamic指令的目的本来就是用于动态语言支持,它所对应的引用称为“动态调用店限定符”,这里“动态”的含义就是必须等到程序时机运行到这条指令的时候,解析动作才能进行。其余可出发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。

解析动作主要针7类符号引用进行

类或接口(CONSTANT_Class_info)字段(CONSTANT_Fieldref_info)类方法(CONSTANT_Methodref_info)接口方法(CONSTANT_InterfaceMethodref_info)方法类型(CONSTANT_MethodType_info)方法句柄(CONSTANT_MethodHandle_info)调用点限定符(CONSTANT_InvokeDynamic_info)

初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载其参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)初始化阶段是执行类构造器()方法的过程。

对于()方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为()方法是带锁线程安全,所以在多线程环境喜爱进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。

对于初始化阶段,虚拟机严格过饭了又切只有5种情况下,必须对类机械能初始化。(参见文章开头)


最新回复(0)