虚拟机类加载机制
什么是虚拟机的类加载机制?
Java虚拟机将描述类的Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程叫做虚拟机的类加载机制
类加载的时机
一个类型(类或接口的可能)从被加载到虚拟机内存中开始啊,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、和卸载这七个过程
其中加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,类型的加载必须按部就班的按照这种顺序开始
,这里的开始强调的是这个阶段通常是交叉混合进行的,会在一个阶段激活另一个阶段;至于解析阶段在某些情况可以在初始化阶段之后再进行,这是为了满足Java语言的运行时绑定特性(动态绑定,晚期绑定)
对于开始类加载过程的第一个阶段“加载”这个过程,并没有强制性要求可以交给虚拟机自行把握,但是严格规定了有且只有六种情况必须进行类的初始化(而初始化之前的阶段自然需要完成)
1.遇到new-使用new关键字进行实例化对象
getstatic、putstatic、invokestatic(读取或设置一个类型的静态字段,或者调用一个类型的静态方法)
这四条字节码指令时,如果类型没有过初始化则必须进行初始化
2、使用Java.lang.reflect包的方法进行反射调用时
3,初始化类时如果其父类还没有初始化则先初始化其父类
4.虚拟机启动时用户需要指定一个要执行的主类,,虚拟机会先初始化这个主类
5.使用java 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的结果为REF_putstatic,REF_getstatic,REF_invokestatic,REF_newinvokeSpecial四种句柄时,则需要先触发初始化
6.如果一个接口定义了java 8新加入的默认方法时,如果一个实现类发生了初始化则需要先初始化接口
类加载的过程
加载
首先需要明确,“加载”阶段是“类加载”过程中的一个阶段
在加载阶段虚拟机需要完成三件事
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结果转化为方法区的运行时数据结构
3.在内存生成一个代表这个类的java.lang.class对象,作为方法区这个类的各个数据的入口
首先第一点,并未明确规定虚拟机需要这个二进制字节流,留给加载阶段构建出一个广阔的舞台,例如
1.从ZIP压缩包中获取,最终称为日后的JAR,EAR,WAR文件格式
2.从网络中获取,最典型的应用就是Web Applet
3.运行时计算生成,这种场景应用最多的是动态代理技术,在java.lang.reflect.Proxy中就是用ProxyGenerateProxyClass()为特定接口生成形式为“*$Proxy”的代理类二进制字节流
4.由其他文件生成,如JSP文件
5.从加密文件中获取,防止class文件被反编译
对于非数组类,加载阶段既可以使用虚拟机内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过自己定义的类加载器去控制字节流 的获取方式,实现根据自己想法获取运行代码的动态性
对于数组类来说,数组类本身不通过类加载器进行创建,它是由java虚拟机直接从内存中动态构造出来的。但是数组类与类加载器还是有很密切的关系,因为数组类的元素类型最终还是靠类加载器来完成加载,数组类的创建规则
1.如果数组的元素类型是引用类型,那就递归采用加载过程去加载这个组件类型,数组C会被标识在加载该组件类型的类加载器的类名称上(一个类型必须与类加载器一起确定为唯一性)
2.如果不是引用类型;例如[]int 的组件类型是int,虚拟机会将数组C标记为与引导类加载器关联
3.数组类的访问性与它的组件可访问性一致,如果组件类型不是引用类型,它的数组可访问性默认为public,可被所有的接口和类访问
加载阶段结束后,虚拟机外部的二进制字节流就按照虚拟机设定的格式存储在方法区中,方法区中的数据格式由虚拟机定义,类型数据安置在方法区后会在java堆内存中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的类型数据的接口
加载阶段与连接阶段的部分动作是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但是夹在夹在阶段中的进行的动作,仍然是连接阶段的一部分,这两个阶段的开始时间仍保持着先后顺序
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》全部约束,保证这些信息被当做代码运行后不会危害虚拟机自身安全。
由于Class文件不一定只能由Java源码编译而来,它可以使用任何二进制字节流,所以为了保护虚拟机自身安全,验证字节流的合法性就很重要了
验证阶段主要包含四个阶段的检验动作 文件格式检验、元数据验证、字节码验证、符号引用验证
1.文件格式检验
首先检验字节码是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,这个主要的验证点有:
主次版本号是否在虚拟机接受的范围内
常量池中的常量是否有不被接受的常量类型
…
第一阶段的验证点还有很多很多,该验证阶段的主要目的是保证输入的字节流能够顺利解析并存储于方法区内,格式上符合一个Java类型信息的要求,只有通过这个阶段的验证,这段字节流才会被允许进入Java虚拟机的方法区中进行存储,所以后面的三个阶段全是在方法区的存储结构上进行操作,不会再直接读取,操作字节流了
2.元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,这个阶段的验证点:
这个类是否有父类(除了java.lang.Object之外,其他的类都有父类)
这个类是否继承了不允许被继承的类(被final修饰的类)
如果这个类不是抽象类是否实现了其父类或接口之中要求实现的所有方法
…
第二阶段主要目的是对类的元数据信息进行语义校验,保证不存在不合《Java语言规范》定义的元数据信息
3.字节码验证
这个阶段是验证阶段中最复杂的一个阶段,主要是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二个阶段对元数据信息中的数据类型校验完毕后就要对类的方法体(Class文件中的code属性)进行校验,保证被校验的类在运行时不会危害Java虚拟机,如
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现“在操作数栈中放置了一个int类型的数据使用时却按照long类型来加载入本地变量表”这种情况
保证任何跳转指令都不会跳转到方法体外
…
如果一个类型中有方法体的字节码没有通过字节码验证,说明这个方法体肯定是有问题的;但如果一个方法体通过了字节码验证也仍然不能保证它是安全的,涉及到离散数学中一个著名的问题–“停机问题”,即不能通过程序准确的判断出一段程序是否存在bug
4.符号引用验证
最后一个阶段的校验行为发生在虚拟机即将将符号引用转化为直接引用,这个转化动作将在连接的第三个阶段–解析中发生。
主要为了验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源
符号引用中通过字符串描述的全限定名能否找到对应的类
在指定的类中是否存在符合方法的的字段描述符及简单名称所描述的方法和字段
…
符号引用验证主要为了确保解析行为能够顺利执行
验证阶段对于虚拟机的类加载机制来说,是非常重要的但却不是必须的,因为验证阶段只有通过或者不通过 的区别,如果程序运行的全部代码都已经被反复使用或验证过,则在生产环境的实施阶段可以考虑使用**-Xverify:none**参数大部分类验证措施以缩短虚拟机类加载时间
准备
准备阶段是正式为类中定义的变量分配内存并设置变量初始值的阶段(被static修饰的变量)
在准备阶段需要注意两点:
首先是这时候进行内存分配的仅包括类变量,而不包括实例变量
实例变量将会在对象实例化时随着对象一起分配在Java堆中
假设一个类变量的定义:
public static int value=123;
那变量在准备阶段过后的初始值是0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是被编译后,存放于类构造器()方法中,所以赋值操作会在初始化阶段才会被执行
public static final int value=123;
如果类字段的字段属性是ConstantValue属性,那么在准备阶段就会被初始化为ConstantValue属性所指定的初始化值,编译时Javac会为value生成ConstantValue属性,在准备阶段将value的值赋为123
解析
解析阶段是Java虚拟机将常量池中的符号引用替换为直接引用的过程
符号引用:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是同一个能间接定位到目标的句柄。如果有了直接引用那么引用的目标一定已经出现在虚拟机的内存中了
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七种符号引用进行
初始化,分别对应常量池中CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info;这里先介绍四种:
1.类或接口解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么虚拟机完成解析阶段需要包括以下三个步骤:
1)如果C不是一个数组类型,那虚拟机会把代表N的全限定名传递给D的类加载器去加载类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他类加载动作,例如加载这个类的父类或接口。一旦加载过程中出现了任何异常那么解析过程也会失败
2)如果C是一个数组类型,并且数组元素类型为对象,那会按照第一点规则加载数组的元素类型,如果N的描述符如“java.lang.Integer类型,接着由虚拟机生成一个代表该数组维度和元素的数组对象
3)如果上面两步都没有异常,那么C在虚拟机就已经成为了一个有效的类或者接口了,但解析完成前还要进行符号引用的验证,确定D是否具备对C的访问权限,如果发现不具备访问权限,则会抛出java.lang.IllegalAccessError异常
针对第三点的权限访问,java 9引入模块化后,一个public类型不再意味着程序任何位置都有它的访问权限,我们还必须检查模块间的访问权限。
如果我们说一个D具有C的访问权限内,就意味着下面三条规则中至少有一条成立
被访问的类C是public的,并且与访问类D处于同一个模块
被访问的类C是public的,不与访问类D处于同一个模块,但是被访问类C的模块是允许访问类D的模块进行访问
被访问的类C不要是public的,但是它与访问类D处于同一个包中
后续涉及到可访问性时都需要考虑模块间访问权限隔离的约束
2.字段解析
要解析一个未被解析过的字段符号引用,首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也是字段所属的类或接口的符号引用,把这个字段所属的类或接口用C表示,对字段的解析规则在《Java虚拟机规范》中明确了以下步骤
1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
2)否则,在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
3)否则,如果C不是java.lang.object的话,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
4)否则查找失败,抛出java.lang.NoSushFieldError异常
如果查找过程中返回了引用则对 这个字段进行权限验证,如果发现不具备对字段的访问权限,则会抛出java.lang.IllegalAccessError异常
3.方法解析
方法解析的第一个步骤与字段解析一样, 也是需要先解析出方法表class_index项中索引的方法所属的类或接口的符号引用,我们仍然用C表示这个类,方法搜索的步骤是:
1)由于Class文件格式中的类的方法和接口的方法符号引用的常量类型的定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出java.lang.IncompatibleClassError异常
2)如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标匹配的方法,如果有则返回这个字段的直接引用,查找结束
3)否则,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
4)否则,在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,说明类C是一个抽象类,这时查找结束,抛出java.lang.AbstractMethodError
5)否则查找失败,抛出java.lang.NoSushMethodError异常
如果查找过程中返回了直接引用则对 这个方法进行权限验证,如果发现不具备对方法的访问权限,则会抛出java.lang.IllegalAccessError异常
4.接口方法分析
初始化
在之前的加载阶段,除了加载阶段中可以通过自定义加载器的方式局部参与外,其余动作都由虚拟机主导。直到初始化阶段,虚拟机才开始真正的执行类中编写的Java程序代码,将主导权交给程序
进行准备阶段时,变量已经有赋过一次初始零值,而在初始化阶段会根据程序编码定制的主观计划去初始化变量和资源。也可以这样理解:初始化阶段就是执行类构造器()方法的过程()并不是程序员在Java代码中直接编码的方法,而是Javac的自然生成物,接下来我们详细了解一下()方法
()是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块可以赋值,但不能访问
class N {static {i = 1;//赋值操作可以正常编译System.out.println(i);//报错,非法向前引用}static int i = 0; }
()方法与类的构造方法(即实例构造器<init()方法>)不同,它不需要显示的调用父类的构造器,Java虚拟机会保证在子类的()方法执行前父类的()方法已经执行完毕。因此虚拟机内第一个执行()方法的肯定是Object类
由于父类的()方法先执行所以父类定义的静态语句块要优先于子类的变量赋值操作
()方法对于类和接口并不是必须的,如果一个类中没有静态语句块和赋值语句,那么编译器可以不为这个类生成()方法
Java虚拟机必须保证一个类的()方法在多线程的环境下正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程需要阻塞等待,直到活动线程的()方法执行完毕,如果在一个类中()方法有很长的耗时操作,那么可能会导致多个进程阻塞
总结
至此我们已经总结了类的加载机制的大体流程
现在来回顾一下,类加载的时机是不确定的,因为类加载的第一步加载这个阶段并没有强制性要求可以交给虚拟机自行把握,但是类加载的顺序是确定的 ,其中加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,类型的加载必须按部就班的按照这种顺序开始,并且明确了初始化的时机
开始类加载的七大阶段有
加载
将类的Class二进制字节流文件加载进虚拟机的方法区内,一遍后续的操作,直接对方法区的数据结构进行读取
验证
验证阶段主要是为了保证字节流信息不会危害虚拟机的安全
验证主要包括
文件格式验证(保证输入的字节流符合Java虚拟机规范,能够被虚拟机所处理,能够将字节流信息顺利解析并存储到方法区内)
元数据验证(对字节码进行语义分析,是否符合《Java语言规范》)
字节码验证(判断程序是否是合法的符合逻辑的)
符号引用验证(校验类的依赖、字段、方法的合法性,确保解析行为能够顺利执行)
准备
给类中定义的变量赋初值(被static修饰的变量)
解析
将常量池中的符号引用转为直接引用
主要包含了解析类或接口、方法、字段、接口方法的合法性以及访问的权限的合法性
初始化
执行类构造器的**()方法**,()方法的功能有
自动收集类中所有类变量的赋值动作和静态语句块中的语句
保证在子类的()方法执行前父类的()方法已经执行完毕
如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程需要阻塞等待
使用
卸载