虚拟机的类加载机制
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟机的类加载机制相关的知识,希望对你有一定的参考价值。
虚拟机的类加载机制
概述
- 虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机使用的Java类型。
- 在Java语言里,类型的加载、连接和初始化过程都是在程序的运行期间完成的。
类加载的时机
- 类的生命周期:加载、连接(验证、准备、解析)、初始化、使用、卸载。
- 类加载过程中加载、验证、准备、初始化和卸载阶段的顺序是可以确定的。但是解析阶段可以在初始化阶段之后再进行(为了支持Java语言的运行时绑定)
- 对于类加载的加载阶段,虚拟机并没有进行强制规定,但是在类加载的初始化阶段,有且仅有下面五种情况必须立即对类进行初始化(而加载、验证、解析自然要在此之前开始)。
- 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(除了被final修饰、已在编译期将结果放入常量池的字段)的时候、以及调用一个类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化则需要进行初始化。
- 当初始化一个类的时候,若其父类还没有进行初始化,则需要先触发其父类的初始化。(接口这点与类不同,在接口中一个接口在初始化时并不要求其父接口全部完成了初始化,只有在正在使用到父接口时才会初始化)
- 当虚拟机启动时,用户需要指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类。
- 使用jdk1.7动态语言时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
类加载的过程
加载
在类的加载阶段虚拟机需要完成下列三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。(可以通过zip包中读取、网络中获取、运行时计算生成、数据库读取等)
- 将这个字节流代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
对于非数组类的加载阶段(主要指加载阶段中获取类的二进制字节流的动作),可以使用系统提供的引导类加载器完成,也可以通过用户自定义的类加载器来完成(即重写一个类加载器的loadClass()方法)。
而对于数组类来说,数组类本身不通过类加载器创建,它由Java虚拟机直接创建。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(指数组去掉所有维度的类型)最终还是要靠类加载器去创建。
数组类的创建规则遵循以下规则:
- 如果数组的组件类型(指数组去掉一个维度的类型)是引用类型,则递归采用上面的加载过程去加载这个组件类型,同时数组类将在加载该组件类型的类加载器的类名称空间上被标识。
- 如果数组类的组件类型不是引用类型(如int[]数组),Java虚拟机将会把数组类标记为与引导类加载器相关联。
- 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型、那么数组类的可见性会默认为public。
加载完成后,虚拟机外部的二进制字节流文件按照虚拟机所需的格式存储在方法区之中(方法区中的数据储存格式由虚拟机自行定义)。然后在内存中实例化一个java.lang.Class类的对象,(对于Hotspot虚拟机来说Class对象比较特殊,它虽然是对象,但是存放在方法区里面)这个对象将作为程序访问方法区中这些类型数据的外部接口。
验证
验证时连接阶段的第一步,目的是确保Class字节流中包括的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。
大致会完成下面几个阶段的验证:
文件格式验证
第一阶段要验证字节流是否符合Class文件格式规范,并能被当前版本的虚拟机处理。比如:
- 是否以魔数0xCAFEBABY开头
- 主、次版本号是否在虚拟机处理范围内
- 常量池的常量是否有不被支持的常量类型
- Class文件中各个部分及文件本身是否有被删除或附加的其它信息
- ……………………
小结:
由此可以看出加载阶段与连接阶段的部分内容有些是交叉进行的(如:加载阶段与文件格式验证动作),但是加载与连接这二个阶段仍保持着固定的先后顺序。
这个阶段是基于二进制字节流进行的,只有通过这个阶段的验证,字节流才会进入内存中的方法区进行储存,所以后面的验证阶段全部是基于方法区的储存结构进行的,不会再字节操作字节流。
元数据验证
第二个阶段主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能验证的内容有:
- 这个类是否有父类
- 这个类的父类是否继承了不允许继承的类
- 若这个类不是抽象类是否实现了其父类或接口中要求实现的所有方法
- ……………………
字节码验证
第三个阶段验证主要是通过数据流和控制流分析、确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验的类的方法在运行时不会危害虚拟机。
如果一个类方法体的字节码没有通过字节码验证,则肯定是有问题的;但是如果一个方法体通过了字节码验证,也不能说明其一定是安全的。
符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个动作将发生在连接的第三阶段——解析阶段,用于确保解析动作能正常执行。
符号引用可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要检验的有:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性(private、public)是否可以被当前类访问。
- ………………
小结:对于虚拟机来说验证阶段是一个重要但非必要的阶段。比如第三方包中的代码已经被反复使用和验证过,则在实施阶段可以使用 -Xverify:none参数来关闭大部分类验证措施,节约虚拟机类加载时间。
准备
准备阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中进行内存分配。
private static int value = 123;
设置类变量初始值通常是指数据的零值,如上述value在准备阶段过后的初始值是0。value赋值为123实在初始化阶段才会执行。
对于某些类字段的字段属性表中存在ConstantValue属性,那么在准备阶段字段将被初始化为ConstantValue所指定的值。例如:
private static final int value = 123;
此时value的值再准备阶段就初始化为123。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:可以是任何形式的字面量,与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,它和虚拟机实现的内存布局相关。若有了直接引用,则引用的目标必定已经在内存中存在。
对同一个符号引用进行多次解析请求是很常见的事情,除invokedynamic指令以外,虚拟机实现可以将第一次解析的结果进行缓存从而避免解析动作重复进行。
而对于invokedynamic指令不必须等到程序实际运行这条指令的时候,解析动作才能进行。相对的,其他的指令可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。
类或接口的解析
当前类为D,把一个从未解析过的符号引用N解析为一个类或接口C的直接引用。
- 若C不是一个数组类型,虚拟机就会把N的全限定名传递给当前类D的类加载器去加载这个类C。在加载过程中又有验证阶段,可能触发相关类的加载动作,一旦加载过程出现异常则解析失败。
- 如果C是一个数组类型,(元素类型:数组去掉所有纬度的类型)
若数组的元素类型为对象(即N的描述符类似于[Ljava/lang/Integer 的形式),则按照第一点规则加载数组元素类型。
若数组的元素类型为基本类型(如java.lang.Integer),则有虚拟机生成一个代表此数组纬度和元素的对象。 - 若上面步骤未出错,且进行符号引用验证,确认D是否有对C的访问权限。若有,则解析成功。
字段解析
解析一个未被解析过的字段引用
- 首先会
对字段所属的类或接口的符号引用进行解析
,解析成功则将这个类或接口用C表示。若C本身包含了简单名称和字段描述符与目标相匹配的字段,则直接返回该字段的直接引用,查找结束。 - 否则,若在C中实现了接口,将会按照继承关系从下往上
搜索各个接口和它的父借口
,若果接口中包含了简单名称和字段描述符与目标相匹配的字段,则直接返回该字段的直接引用,查找结束。 - 否则,如果C类不是java.lang.Object的话,将会
按照继承关系从下往上递归搜索其父类
,若在父类中包含了简单名称和字段描述符与目标相匹配的字段,则直接返回该字段的直接引用,查找结束。 - 否则,查找失败,抛出java.lang.NoSuchFieldError异常
- 若查找过程中成功反悔了该字段的直接引用,将会
对该字段进行权限验证
,若发现没有访问权限,抛出java.lang.IlleagalAccessError异常。。
类方法解析
解析类方法对应的类或接口的符号引用
,解析成功用C表示这个类。- 若在类方法中发现class_index中索引的C是个接口而不是类,则抛出异常。(因为类方法与接口方法符号引用的常量类型定义是分开的)
- 否则,在
类C中查找
是否存在简单名称及描述符与目标相匹配的方法,有则返回直接引用,结束。 - 否则,在
类C的父类中查找
是否存在简单名称及描述符与目标相匹配的方法,有则返回直接引用,结束。 - 否则,在
类C的接口及其父类接口查找
是否存在简单名称及描述符与目标相匹配的方法,若有,则说明类C是一个抽象类,查找结束抛出异常。 - 若上述步骤都没找到则查找失败,抛出异常;若查找成功,则进
行权限验证
。
接口方法解析
- 解析接口方法对应的类或接口的符号引用,解析成功用C表示这个接口。
- 若在接口方法表中发现class_index中索引的C是个类而不是接口,则抛出异常。
- 否则,
在接口C中查找
是否存在简单名称及描述符与目标相匹配的方法,有则返回直接引用,结束。 - 否则,
在接口C的父接口中查找
是否存在简单名称及描述符与目标相匹配的方法,有则返回直接引用,结束。 - 否则,查找失败,抛出异常。
注意:因为接口中的方法都是public的所以不需要进行权限验证。
初始化
类初始化是类加载的最后一步,此时才开始真正执行Java代码(字节码)。
初始化也可看成执行类构造器< clinit >()方法的过程。(注意与构造函数< init >()方法的区别)它有以下一些特点:
- < clinit >()方法与类的构造函数(实例构造器< init >()方法)不同,不需要显示调用父类构造器,虚拟机会保证在子类的< clinit >()方法执行前,父类的< clinit >()方法已经执行完毕。
- < clinit >()方法中是编译器自动收集类中的所有类变量的赋值动作和静态语句块。收集顺序是语句在原文件中出现的顺序,所以定义在后面的变量可以赋值,但不能在定义位置前去访问。
{
static{
i=0;
System.out.println(i);//这句会报错,不能再定义之前去访问
}
static int i = 1;
}
- 由于父类的< clinit >()方法先执行,所以父类的类变量赋值及静态语句块优于子类的静态变量赋值。
- 若一个类中没有静态语句块或对变量的赋值操作,则可以不为这个类生产< clinit >()方法。
- 执行接口的< clinit >()方法不需要先执行父接口的< clinit >()方法,只有当父接口的变量使用时,才会初始化。另外,接口的实现类初始化时也不会执行借口的< clinit >()方法。
- 虚拟机会保证< clinit >()方法在多线程环境中被正确的加锁、同步。
类加载器
通过一个类的全限定名来获取来获取描述此类的二进制字节流,这个动作的代码模块称为“类加载器”。(在Java虚拟机外部实现)
以上是关于虚拟机的类加载机制的主要内容,如果未能解决你的问题,请参考以下文章